spora 0.7.7 → 0.7.8

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.
Files changed (46) hide show
  1. package/dist/{autonomy-NNFTM5NW.js → autonomy-ZMFZRXDZ.js} +7 -7
  2. package/dist/{chunk-BBXHECZ5.js → chunk-6WBIVXOY.js} +1 -1
  3. package/dist/chunk-6WBIVXOY.js.map +1 -0
  4. package/dist/{chunk-JIMONWKO.js → chunk-73CWOI44.js} +4 -4
  5. package/dist/chunk-73CWOI44.js.map +1 -0
  6. package/dist/{chunk-ZLSDFYBR.js → chunk-AIGSCHZK.js} +2 -2
  7. package/dist/{chunk-CP6JWCLY.js → chunk-ER6TILYZ.js} +1 -25
  8. package/dist/{chunk-CP6JWCLY.js.map → chunk-ER6TILYZ.js.map} +1 -1
  9. package/dist/chunk-OLYPPXKP.js +69 -0
  10. package/dist/chunk-OLYPPXKP.js.map +1 -0
  11. package/dist/{chunk-5R4AJZHN.js → chunk-TKGB5LIN.js} +2 -2
  12. package/dist/chunk-TTDQZI5W.js +1699 -0
  13. package/dist/chunk-TTDQZI5W.js.map +1 -0
  14. package/dist/cli.js +28 -28
  15. package/dist/{client-AR5ZD6S4.js → client-3APKWQ6O.js} +3 -3
  16. package/dist/{colony-UGVYALOS.js → colony-7GZ2ODF2.js} +2 -2
  17. package/dist/{heartbeat-WJJSGUAQ.js → heartbeat-CUM7FIHS.js} +23 -7
  18. package/dist/heartbeat-CUM7FIHS.js.map +1 -0
  19. package/dist/heartbeat-narrative-B3RD3OPJ.js +11 -0
  20. package/dist/{init-6HY4ZPFJ.js → init-KL6XSI7E.js} +3 -3
  21. package/dist/mcp-server.js +20 -20
  22. package/dist/{memory-DTSLVSQG.js → memory-G4DNIGLT.js} +2 -2
  23. package/dist/{prompt-builder-ZFUZNQY2.js → prompt-builder-S6PJVEC5.js} +4 -4
  24. package/dist/{queue-QCGNDHH2.js → queue-YPBUUP22.js} +2 -2
  25. package/dist/web-chat/chat.html +15 -5
  26. package/dist/{web-chat-AKUEBSWS.js → web-chat-XNTIDKAS.js} +37 -15
  27. package/dist/web-chat-XNTIDKAS.js.map +1 -0
  28. package/dist/{x-client-S2LUVEKV.js → x-client-2HFEHHVE.js} +2 -2
  29. package/dist/x-client-2HFEHHVE.js.map +1 -0
  30. package/package.json +1 -1
  31. package/dist/chunk-BBXHECZ5.js.map +0 -1
  32. package/dist/chunk-JIMONWKO.js.map +0 -1
  33. package/dist/chunk-TTM54LQR.js +0 -2769
  34. package/dist/chunk-TTM54LQR.js.map +0 -1
  35. package/dist/heartbeat-WJJSGUAQ.js.map +0 -1
  36. package/dist/web-chat-AKUEBSWS.js.map +0 -1
  37. /package/dist/{autonomy-NNFTM5NW.js.map → autonomy-ZMFZRXDZ.js.map} +0 -0
  38. /package/dist/{chunk-ZLSDFYBR.js.map → chunk-AIGSCHZK.js.map} +0 -0
  39. /package/dist/{chunk-5R4AJZHN.js.map → chunk-TKGB5LIN.js.map} +0 -0
  40. /package/dist/{client-AR5ZD6S4.js.map → client-3APKWQ6O.js.map} +0 -0
  41. /package/dist/{colony-UGVYALOS.js.map → colony-7GZ2ODF2.js.map} +0 -0
  42. /package/dist/{memory-DTSLVSQG.js.map → heartbeat-narrative-B3RD3OPJ.js.map} +0 -0
  43. /package/dist/{init-6HY4ZPFJ.js.map → init-KL6XSI7E.js.map} +0 -0
  44. /package/dist/{prompt-builder-ZFUZNQY2.js.map → memory-G4DNIGLT.js.map} +0 -0
  45. /package/dist/{queue-QCGNDHH2.js.map → prompt-builder-S6PJVEC5.js.map} +0 -0
  46. /package/dist/{x-client-S2LUVEKV.js.map → queue-YPBUUP22.js.map} +0 -0
@@ -1,2769 +0,0 @@
1
- import {
2
- getXClient
3
- } from "./chunk-5R4AJZHN.js";
4
- import {
5
- addToQueue
6
- } from "./chunk-ZLSDFYBR.js";
7
- import {
8
- buildOpportunityPortfolioMessage,
9
- buildPersonaConstraintLines,
10
- buildSystemPrompt,
11
- compilePersonaActionProfile,
12
- getPersonaConstraints,
13
- personaConstraintHandles
14
- } from "./chunk-JIMONWKO.js";
15
- import {
16
- listIntents,
17
- loadStrategy,
18
- recordIntentExecution
19
- } from "./chunk-OTZNHIXT.js";
20
- import {
21
- getActiveTrackedPosts,
22
- retireOldPosts,
23
- updatePostMetrics
24
- } from "./chunk-CP6JWCLY.js";
25
- import {
26
- loadIdentity,
27
- saveIdentity
28
- } from "./chunk-IULO3GRE.js";
29
- import {
30
- loadCredentials
31
- } from "./chunk-SXNZVKLJ.js";
32
- import {
33
- generateResponse
34
- } from "./chunk-342ZX72W.js";
35
- import {
36
- logger
37
- } from "./chunk-RSNEVBEI.js";
38
- import {
39
- addLearning,
40
- getRecentInteractions,
41
- loadRelationships,
42
- logInteraction
43
- } from "./chunk-BBXHECZ5.js";
44
- import {
45
- paths
46
- } from "./chunk-ZWKTKWS6.js";
47
-
48
- // src/runtime/decision-engine.ts
49
- async function executeAction(action) {
50
- const { action: type } = action;
51
- try {
52
- switch (type) {
53
- case "post": {
54
- if (!action.content) return { action: type, success: false, error: "No content provided" };
55
- if (action.content.length > 280) {
56
- return { action: type, success: false, error: `Tweet too long: ${action.content.length} chars (max 280)` };
57
- }
58
- const client = await getXClient();
59
- const result = await client.postTweet(action.content);
60
- if (result.success) logger.info(`Posted: "${action.content.slice(0, 50)}..."`);
61
- return { action: type, success: result.success, detail: result.tweetId, error: result.error };
62
- }
63
- case "reply": {
64
- if (!action.tweetId || !action.content) {
65
- return { action: type, success: false, error: "Missing tweetId or content" };
66
- }
67
- const client = await getXClient();
68
- const result = await client.replyToTweet(action.tweetId, action.content);
69
- if (result.success) logger.info(`Replied to ${action.tweetId}: "${action.content.slice(0, 50)}..."`);
70
- return { action: type, success: result.success, detail: result.tweetId, error: result.error };
71
- }
72
- case "like": {
73
- if (!action.tweetId) return { action: type, success: false, error: "Missing tweetId" };
74
- const client = await getXClient();
75
- const result = await client.likeTweet(action.tweetId);
76
- return { action: type, success: result.success, error: result.error };
77
- }
78
- case "retweet": {
79
- if (!action.tweetId) return { action: type, success: false, error: "Missing tweetId" };
80
- const client = await getXClient();
81
- const result = await client.retweet(action.tweetId);
82
- return { action: type, success: result.success, error: result.error };
83
- }
84
- case "follow": {
85
- if (!action.handle) return { action: type, success: false, error: "Missing handle" };
86
- const client = await getXClient();
87
- const result = await client.followUser(action.handle);
88
- if (!result.success) {
89
- logInteraction({
90
- id: `int-${Date.now()}`,
91
- timestamp: (/* @__PURE__ */ new Date()).toISOString(),
92
- type: "follow",
93
- targetHandle: action.handle.replace(/^@/, ""),
94
- creditsUsed: 0,
95
- success: false,
96
- error: result.error
97
- });
98
- }
99
- return { action: type, success: result.success, error: result.error };
100
- }
101
- case "schedule": {
102
- if (!action.content) return { action: type, success: false, error: "No content" };
103
- const entry = addToQueue(action.content);
104
- logger.info(`Scheduled: "${action.content.slice(0, 50)}..." for ${entry.scheduledFor}`);
105
- return { action: type, success: true, detail: `Scheduled for ${entry.scheduledFor}` };
106
- }
107
- case "learn": {
108
- if (!action.content) return { action: type, success: false, error: "No content" };
109
- addLearning(action.content, "agent", action.tags ?? ["heartbeat"]);
110
- logger.info(`Learned: "${action.content.slice(0, 50)}..."`);
111
- return { action: type, success: true };
112
- }
113
- case "reflect": {
114
- if (!action.content) return { action: type, success: false, error: "No content" };
115
- const identity = loadIdentity();
116
- identity.evolutionJournal.push({
117
- date: (/* @__PURE__ */ new Date()).toISOString(),
118
- reflection: action.content
119
- });
120
- saveIdentity(identity);
121
- logger.info(`Reflected: "${action.content.slice(0, 50)}..."`);
122
- return { action: type, success: true };
123
- }
124
- case "skip": {
125
- logger.info(`Skipping: ${action.reason ?? action.reasoning ?? "no reason given"}`);
126
- return { action: type, success: true, detail: action.reason ?? action.reasoning };
127
- }
128
- default:
129
- logger.warn(`Unknown action: ${type}`);
130
- return { action: type, success: false, error: `Unknown action: ${type}` };
131
- }
132
- } catch (error) {
133
- const msg = error.message;
134
- logger.error(`Action ${type} failed: ${msg}`);
135
- return { action: type, success: false, error: msg };
136
- }
137
- }
138
-
139
- // src/runtime/policy.ts
140
- function normalize(text) {
141
- return text.toLowerCase().replace(/https?:\/\/\S+/g, "").replace(/[@#]\w+/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
142
- }
143
- function tokenSet(text) {
144
- const tokens = normalize(text).split(" ").filter(Boolean);
145
- return new Set(tokens);
146
- }
147
- function jaccardSimilarity(a, b) {
148
- const aSet = tokenSet(a);
149
- const bSet = tokenSet(b);
150
- if (aSet.size === 0 || bSet.size === 0) return 0;
151
- let overlap = 0;
152
- for (const token of aSet) {
153
- if (bSet.has(token)) overlap += 1;
154
- }
155
- const union = aSet.size + bSet.size - overlap;
156
- return union === 0 ? 0 : overlap / union;
157
- }
158
- function firstWords(text, n) {
159
- return normalize(text).split(" ").filter(Boolean).slice(0, n).join(" ");
160
- }
161
- function hasAllCapsEnding(text) {
162
- const ending = text.split(/[.!?]/).map((s) => s.trim()).filter(Boolean).slice(-1)[0] ?? "";
163
- const words = ending.split(/\s+/).filter(Boolean);
164
- if (words.length < 3) return false;
165
- return words.every((word) => /^[A-Z0-9]+$/.test(word));
166
- }
167
- function wordCount(text) {
168
- return normalize(text).split(" ").filter(Boolean).length;
169
- }
170
- function sentenceCount(text) {
171
- return text.split(/[.!?]/).map((part) => part.trim()).filter(Boolean).length;
172
- }
173
- function explanatoryMarkerHits(text) {
174
- const lower = text.toLowerCase();
175
- const markers = [
176
- "the real issue",
177
- "the core issue",
178
- "in other words",
179
- "the point is",
180
- "what this means",
181
- "the lesson",
182
- "therefore",
183
- "ultimately",
184
- "meanwhile",
185
- "this reveals",
186
- "this proves"
187
- ];
188
- return markers.reduce((hits, marker) => hits + (lower.includes(marker) ? 1 : 0), 0);
189
- }
190
- function soundsLikeLecture(content) {
191
- const words = wordCount(content);
192
- const commas = (content.match(/,/g) ?? []).length;
193
- const semicolons = (content.match(/;/g) ?? []).length;
194
- const hasQuestion = content.includes("?");
195
- const markerHits = explanatoryMarkerHits(content);
196
- if (words > 58) return true;
197
- if (words > 42 && commas + semicolons >= 3 && !hasQuestion) return true;
198
- if (markerHits >= 2) return true;
199
- return false;
200
- }
201
- function soundsTooHedgedForPersona(content) {
202
- try {
203
- const identity = loadIdentity();
204
- const assertive = identity.traits.confidence >= 0.72 || identity.traits.aggression >= 0.62 || identity.conflictStyle === "clap-back" || identity.conflictStyle === "debate";
205
- if (!assertive) return false;
206
- const lower = content.toLowerCase();
207
- const hedges = [
208
- "you're both right",
209
- "both can be true",
210
- "on one hand",
211
- "on the other hand",
212
- "it depends",
213
- "it's complicated",
214
- "nuance",
215
- "fair point from both"
216
- ];
217
- return hedges.some((phrase) => lower.includes(phrase));
218
- } catch {
219
- return false;
220
- }
221
- }
222
- function allowsPhilosopherCadence() {
223
- try {
224
- const identity = loadIdentity();
225
- if (identity.framework === "philosopher") return true;
226
- if (identity.framework === "truthseeker") return false;
227
- return false;
228
- } catch {
229
- return false;
230
- }
231
- }
232
- function philosophicalPatternHits(content) {
233
- const lower = content.toLowerCase();
234
- const markers = [
235
- "the real question",
236
- "the deeper question",
237
- "what becomes possible",
238
- "is just the start",
239
- "the future isn't about",
240
- "the future is not about",
241
- "when intelligence becomes",
242
- "protocols are infrastructure",
243
- "the core future",
244
- "the true question",
245
- "value might emerge",
246
- "isn't about",
247
- "is not about",
248
- "it's about",
249
- "just the start"
250
- ];
251
- return markers.reduce((hits, marker) => hits + (lower.includes(marker) ? 1 : 0), 0);
252
- }
253
- function abstractWordCount(content) {
254
- const words = normalize(content).split(" ").filter(Boolean);
255
- const abstractWords = /* @__PURE__ */ new Set([
256
- "infrastructure",
257
- "protocol",
258
- "protocols",
259
- "architecture",
260
- "architectures",
261
- "consciousness",
262
- "economy",
263
- "economics",
264
- "currency",
265
- "systems",
266
- "system",
267
- "future",
268
- "intelligence",
269
- "optimization",
270
- "abstraction",
271
- "paradigm",
272
- "emerge",
273
- "emergence",
274
- "governs",
275
- "governance"
276
- ]);
277
- return words.filter((word) => abstractWords.has(word)).length;
278
- }
279
- function usesManifestoPunctuation(content) {
280
- return content.includes(":") || content.includes(";") || content.includes("\u2014");
281
- }
282
- var OVERLAP_STOPWORDS = /* @__PURE__ */ new Set([
283
- "this",
284
- "that",
285
- "with",
286
- "from",
287
- "have",
288
- "what",
289
- "when",
290
- "where",
291
- "which",
292
- "your",
293
- "youre",
294
- "they",
295
- "them",
296
- "about",
297
- "into",
298
- "over",
299
- "under",
300
- "while",
301
- "just",
302
- "like",
303
- "really",
304
- "there",
305
- "their",
306
- "then",
307
- "than",
308
- "because",
309
- "would",
310
- "could",
311
- "should",
312
- "being",
313
- "been",
314
- "it's",
315
- "its",
316
- "the",
317
- "and",
318
- "for",
319
- "are",
320
- "was",
321
- "were",
322
- "will",
323
- "not",
324
- "but",
325
- "you",
326
- "our",
327
- "out",
328
- "all",
329
- "too",
330
- "can",
331
- "get",
332
- "got",
333
- "any"
334
- ]);
335
- function meaningfulTokenSet(text) {
336
- const tokens = normalize(text).split(" ").filter((token) => token.length >= 4 && !OVERLAP_STOPWORDS.has(token));
337
- return new Set(tokens);
338
- }
339
- function hasMeaningfulContextOverlap(content, targetText) {
340
- const a = meaningfulTokenSet(content);
341
- const b = meaningfulTokenSet(targetText);
342
- if (a.size === 0 || b.size === 0) return false;
343
- for (const token of a) {
344
- if (b.has(token)) return true;
345
- }
346
- return false;
347
- }
348
- function recentWrittenContent() {
349
- const recent = getRecentInteractions(40);
350
- return recent.filter((i) => i.type === "post" || i.type === "reply").map((i) => i.content ?? "").filter((content) => content.length > 0);
351
- }
352
- var REPETITION_STOPWORDS = /* @__PURE__ */ new Set([
353
- "about",
354
- "after",
355
- "again",
356
- "against",
357
- "almost",
358
- "always",
359
- "among",
360
- "because",
361
- "being",
362
- "between",
363
- "could",
364
- "every",
365
- "first",
366
- "found",
367
- "from",
368
- "going",
369
- "great",
370
- "have",
371
- "having",
372
- "here",
373
- "into",
374
- "just",
375
- "like",
376
- "make",
377
- "more",
378
- "most",
379
- "much",
380
- "only",
381
- "other",
382
- "over",
383
- "really",
384
- "same",
385
- "some",
386
- "such",
387
- "than",
388
- "that",
389
- "their",
390
- "there",
391
- "these",
392
- "they",
393
- "this",
394
- "those",
395
- "through",
396
- "time",
397
- "very",
398
- "what",
399
- "when",
400
- "where",
401
- "which",
402
- "while",
403
- "with",
404
- "would",
405
- "your",
406
- "youre",
407
- "dont",
408
- "cant",
409
- "will",
410
- "shall",
411
- "should",
412
- "also",
413
- "tweet",
414
- "tweets",
415
- "thread",
416
- "threads",
417
- "future",
418
- "human",
419
- "humans",
420
- "technology",
421
- "tech",
422
- "build",
423
- "building",
424
- "system",
425
- "systems",
426
- "agent",
427
- "agents"
428
- ]);
429
- function informativeTokens(text) {
430
- return normalize(text).split(" ").filter((token) => token.length >= 5 && !REPETITION_STOPWORDS.has(token));
431
- }
432
- function findOverusedAnchor(content, recent) {
433
- const recentWindow = recent.slice(0, 12);
434
- if (recentWindow.length < 5) return null;
435
- const tokenCounts = /* @__PURE__ */ new Map();
436
- const bigramCounts = /* @__PURE__ */ new Map();
437
- for (const sample of recentWindow) {
438
- const tokens = informativeTokens(sample);
439
- const uniqueTokens = new Set(tokens);
440
- for (const token of uniqueTokens) {
441
- tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
442
- }
443
- for (let i = 0; i < tokens.length - 1; i += 1) {
444
- const bigram = `${tokens[i]} ${tokens[i + 1]}`;
445
- bigramCounts.set(bigram, (bigramCounts.get(bigram) ?? 0) + 1);
446
- }
447
- }
448
- const contentTokens = informativeTokens(content);
449
- const contentTokenSet = new Set(contentTokens);
450
- for (const token of contentTokenSet) {
451
- const count = tokenCounts.get(token) ?? 0;
452
- if (count >= 4) return token;
453
- }
454
- for (let i = 0; i < contentTokens.length - 1; i += 1) {
455
- const bigram = `${contentTokens[i]} ${contentTokens[i + 1]}`;
456
- const count = bigramCounts.get(bigram) ?? 0;
457
- if (count >= 3) return bigram;
458
- }
459
- return null;
460
- }
461
- function openingFragment(text, words = 4) {
462
- return normalize(text).split(" ").filter(Boolean).slice(0, words).join(" ");
463
- }
464
- function findOverusedOpening(content, recent) {
465
- const opening = openingFragment(content, 4);
466
- if (!opening || opening.split(" ").length < 3) return null;
467
- let matches = 0;
468
- for (const sample of recent.slice(0, 12)) {
469
- if (openingFragment(sample, 4) === opening) {
470
- matches += 1;
471
- }
472
- }
473
- return matches >= 2 ? opening : null;
474
- }
475
- function vocabularyNoveltyRatio(content, recent) {
476
- const current = new Set(informativeTokens(content));
477
- if (current.size === 0) return 1;
478
- const seen = /* @__PURE__ */ new Set();
479
- for (const sample of recent.slice(0, 14)) {
480
- for (const token of informativeTokens(sample)) {
481
- seen.add(token);
482
- }
483
- }
484
- let fresh = 0;
485
- for (const token of current) {
486
- if (!seen.has(token)) fresh += 1;
487
- }
488
- return fresh / current.size;
489
- }
490
- function looksOverAbstractWithoutAnchor(content) {
491
- const abstractCount = abstractWordCount(content);
492
- if (abstractCount < 4) return false;
493
- const words = wordCount(content);
494
- const density = abstractCount / Math.max(1, words);
495
- if (density < 0.2) return false;
496
- const lower = content.toLowerCase();
497
- const hasHandle = /@\w+/.test(content);
498
- const hasNumber = /\b\d+(\.\d+)?\b/.test(content);
499
- const hasQuote = /["“”']/.test(content);
500
- const hasSpecificFigure = /\b(musk|elon|zuck|altman|openai|tesla|spacex|meta|google|apple)\b/.test(lower);
501
- const hasQuestion = content.includes("?");
502
- return !(hasHandle || hasNumber || hasQuote || hasSpecificFigure || hasQuestion);
503
- }
504
- function hasStrongConversationOpportunity(timeline, mentions) {
505
- if (mentions.length > 0) return true;
506
- return timeline.some((tweet) => (tweet.replyCount ?? 0) > 0 || tweet.text.includes("?"));
507
- }
508
- function wasInteractionAction(action) {
509
- return ["reply", "like", "retweet", "follow"].includes(action.action);
510
- }
511
- function isWritingAction(action) {
512
- return ["post", "reply", "schedule"].includes(action.action);
513
- }
514
- function normalizeHandle(handle) {
515
- return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
516
- }
517
- function resolveActionTargetHandle(action, tweetById) {
518
- if (action.handle) return normalizeHandle(action.handle);
519
- if (action.targetHandle) return normalizeHandle(action.targetHandle);
520
- if (action.tweetId) return normalizeHandle(tweetById.get(action.tweetId)?.authorHandle);
521
- return "";
522
- }
523
- function executedWrittenContent(executedActions) {
524
- return executedActions.filter((a) => isWritingAction(a) && typeof a.content === "string").map((a) => a.content?.trim() ?? "").filter((content) => content.length > 0);
525
- }
526
- function nearExactDuplicate(content, recent) {
527
- const normalized = normalize(content);
528
- if (!normalized) return false;
529
- return recent.some((r) => {
530
- const candidate = normalize(r);
531
- if (!candidate) return false;
532
- if (candidate === normalized) return true;
533
- return jaccardSimilarity(content, r) >= 0.88;
534
- });
535
- }
536
- function isDuplicateTarget(action, executedActions) {
537
- if (!action.tweetId) return false;
538
- return executedActions.some((a) => a.tweetId === action.tweetId && a.action === action.action);
539
- }
540
- function repeatedTemplate(content, recent) {
541
- const prefix = firstWords(content, 7);
542
- if (!prefix) return false;
543
- return recent.some((r) => {
544
- const sameStart = firstWords(r, 7) === prefix;
545
- const similar = jaccardSimilarity(content, r) >= 0.62;
546
- return sameStart || similar;
547
- });
548
- }
549
- function overusingAllCapsCadence(content, recentEntries) {
550
- if (!hasAllCapsEnding(content)) return false;
551
- const recentCaps = recentEntries.filter((i) => i.type === "post" && i.content).slice(0, 8).filter((i) => hasAllCapsEnding(i.content ?? ""));
552
- return recentCaps.length >= 2;
553
- }
554
- function hasRecentlyRepliedToTweet(tweetId) {
555
- const recent = getRecentInteractions(200);
556
- const nowMs = Date.now();
557
- const cooldownMs = 24 * 60 * 60 * 1e3;
558
- return recent.some((entry) => {
559
- if (entry.type !== "reply") return false;
560
- if (entry.inReplyTo !== tweetId) return false;
561
- const ts = Date.parse(entry.timestamp);
562
- if (Number.isNaN(ts)) return true;
563
- return nowMs - ts < cooldownMs;
564
- });
565
- }
566
- function hasRecentlyFollowedHandle(handle) {
567
- const clean = normalizeHandle(handle);
568
- if (!clean) return false;
569
- const recent = getRecentInteractions(300);
570
- const nowMs = Date.now();
571
- const cooldownMs = 7 * 24 * 60 * 60 * 1e3;
572
- return recent.some((entry) => {
573
- if (entry.type !== "follow") return false;
574
- if (normalizeHandle(entry.targetHandle) !== clean) return false;
575
- const ts = Date.parse(entry.timestamp);
576
- if (Number.isNaN(ts)) return true;
577
- return nowMs - ts < cooldownMs;
578
- });
579
- }
580
- function evaluateActionPolicy(context) {
581
- const {
582
- action,
583
- step,
584
- timeline,
585
- mentions,
586
- executedActions,
587
- observedTweetIds,
588
- observedTweets,
589
- selfHandle,
590
- selfUserId
591
- } = context;
592
- const self = normalizeHandle(selfHandle);
593
- const selfId = (selfUserId ?? "").trim();
594
- const knownTweets = observedTweets ?? [...timeline, ...mentions];
595
- const tweetById = new Map(knownTweets.map((tweet) => [tweet.id, tweet]));
596
- const constraints = getPersonaConstraints();
597
- const strictReplyHandles = new Set(constraints.onlyReplyToHandles.map((handle) => normalizeHandle(handle)));
598
- const strictInteractHandles = new Set(personaConstraintHandles(constraints).map((handle) => normalizeHandle(handle)));
599
- const targetHandle = resolveActionTargetHandle(action, tweetById);
600
- if (constraints.replyOnlyMode && !["reply", "skip", "learn", "reflect"].includes(action.action)) {
601
- return {
602
- allowed: false,
603
- reason: "Persona is in reply-only mode. Use reply actions only."
604
- };
605
- }
606
- if (constraints.noOriginalPosts && (action.action === "post" || action.action === "schedule")) {
607
- return {
608
- allowed: false,
609
- reason: "Persona forbids standalone original posts."
610
- };
611
- }
612
- if (strictReplyHandles.size > 0) {
613
- if (action.action !== "reply") {
614
- return {
615
- allowed: false,
616
- reason: `Persona requires replying only to specific target(s): @${[...strictReplyHandles].join(", @")}.`
617
- };
618
- }
619
- if (!targetHandle || !strictReplyHandles.has(targetHandle)) {
620
- return {
621
- allowed: false,
622
- reason: `Reply target must be one of @${[...strictReplyHandles].join(", @")}.`
623
- };
624
- }
625
- }
626
- if (strictInteractHandles.size > 0 && ["reply", "like", "retweet", "follow"].includes(action.action)) {
627
- if (!targetHandle || !strictInteractHandles.has(targetHandle)) {
628
- return {
629
- allowed: false,
630
- reason: `Interaction target must be one of @${[...strictInteractHandles].join(", @")}.`
631
- };
632
- }
633
- }
634
- if (isDuplicateTarget(action, executedActions)) {
635
- return { allowed: false, reason: `Action ${action.action} already executed for tweet ${action.tweetId} this heartbeat.` };
636
- }
637
- if (action.content && isWritingAction(action)) {
638
- if (action.content.length > 280) {
639
- return {
640
- allowed: false,
641
- reason: `Content is too long (${action.content.length}/280). Keep it tighter and more conversational.`
642
- };
643
- }
644
- if (action.action === "reply" && wordCount(action.content) > 45 && !action.content.includes("?")) {
645
- return {
646
- allowed: false,
647
- reason: "Reply is too monologue-like. Keep it shorter or ask a direct question."
648
- };
649
- }
650
- if (soundsLikeLecture(action.content)) {
651
- return {
652
- allowed: false,
653
- reason: "Rejected lecture-like writing style. Use a more human, reactive, conversational tone."
654
- };
655
- }
656
- if (soundsTooHedgedForPersona(action.content)) {
657
- return {
658
- allowed: false,
659
- reason: "Rejected hedged consensus phrasing. Your persona should be more decisive and opinionated."
660
- };
661
- }
662
- if (!allowsPhilosopherCadence()) {
663
- const phiHits = philosophicalPatternHits(action.content);
664
- const abstractCount = abstractWordCount(action.content);
665
- const hasQuestion = action.content.includes("?");
666
- if (phiHits >= 1 || abstractCount >= 5 && hasQuestion) {
667
- return {
668
- allowed: false,
669
- reason: "Rejected abstract philosopher cadence. Use concrete, contextual language tied to the actual tweet."
670
- };
671
- }
672
- const words = wordCount(action.content);
673
- const sentences = sentenceCount(action.content);
674
- if (action.action === "reply" && (words > 34 || sentences > 2)) {
675
- return {
676
- allowed: false,
677
- reason: "Reply is too long/explanatory for this persona. Keep it short and direct."
678
- };
679
- }
680
- if (action.action === "post" && (words > 42 || sentences > 3)) {
681
- return {
682
- allowed: false,
683
- reason: "Post is drifting into manifesto style. Keep it more concrete and human."
684
- };
685
- }
686
- if ((action.action === "reply" || action.action === "post") && usesManifestoPunctuation(action.content) && words > 16) {
687
- return {
688
- allowed: false,
689
- reason: "Rejected manifesto punctuation style. Avoid colon/semicolon/em-dash framing."
690
- };
691
- }
692
- }
693
- const existingInHeartbeat = executedWrittenContent(executedActions);
694
- if (nearExactDuplicate(action.content, existingInHeartbeat)) {
695
- return {
696
- allowed: false,
697
- reason: "Rejected duplicate wording in this heartbeat. Write a distinct message before posting/replying again."
698
- };
699
- }
700
- const recent = recentWrittenContent();
701
- const overusedAnchor = findOverusedAnchor(action.content, recent);
702
- if (overusedAnchor) {
703
- return {
704
- allowed: false,
705
- reason: `Rejected repetitive anchor phrase/term "${overusedAnchor}". Use a different framing and vocabulary.`
706
- };
707
- }
708
- const overusedOpening = findOverusedOpening(action.content, recent);
709
- if (overusedOpening) {
710
- return {
711
- allowed: false,
712
- reason: `Rejected repetitive opening phrase "${overusedOpening}". Start from a fresh angle.`
713
- };
714
- }
715
- if (recent.length >= 6 && wordCount(action.content) >= 10) {
716
- const novelty = vocabularyNoveltyRatio(action.content, recent);
717
- if (novelty < 0.34) {
718
- return {
719
- allowed: false,
720
- reason: "Rejected low-novelty vocabulary loop. Use fresher words, examples, or a different conversational angle."
721
- };
722
- }
723
- }
724
- if (!allowsPhilosopherCadence() && looksOverAbstractWithoutAnchor(action.content)) {
725
- return {
726
- allowed: false,
727
- reason: "Rejected abstract wording without concrete anchor. Reference a specific person, event, quote, or question."
728
- };
729
- }
730
- }
731
- const hasConversationOpportunity = hasStrongConversationOpportunity(timeline, mentions);
732
- const interactedAlready = executedActions.some(wasInteractionAction);
733
- const observedCount = observedTweetIds && observedTweetIds.length > 0 ? observedTweetIds.length : timeline.length + mentions.length;
734
- if ((action.action === "reply" || action.action === "like" || action.action === "retweet") && observedCount === 0) {
735
- return {
736
- allowed: false,
737
- reason: "No tweets observed in current context. Avoid blind interactions; gather timeline/search context first."
738
- };
739
- }
740
- if (action.action === "post" && hasConversationOpportunity && !interactedAlready && step < 2) {
741
- return {
742
- allowed: false,
743
- reason: "Engage first: reply/like/follow when mentions or active conversations are available before posting an original tweet."
744
- };
745
- }
746
- if (isWritingAction(action) && action.content) {
747
- const recent = recentWrittenContent();
748
- if (nearExactDuplicate(action.content, recent)) {
749
- return {
750
- allowed: false,
751
- reason: "Rejected near-duplicate content from recent history. Tailor this message to the specific context."
752
- };
753
- }
754
- if (repeatedTemplate(action.content, recent)) {
755
- return {
756
- allowed: false,
757
- reason: "Rejected repetitive content pattern. Use a more novel structure or engage directly with timeline context."
758
- };
759
- }
760
- }
761
- if ((action.action === "post" || action.action === "schedule") && action.content) {
762
- const recentInteractions = getRecentInteractions(20);
763
- if (overusingAllCapsCadence(action.content, recentInteractions)) {
764
- return {
765
- allowed: false,
766
- reason: "Rejected repetitive all-caps slogan cadence. Vary style and reduce monologue formatting."
767
- };
768
- }
769
- }
770
- if ((action.action === "reply" || action.action === "like" || action.action === "retweet") && action.tweetId) {
771
- const known = new Set(observedTweetIds ?? [...timeline, ...mentions].map((tweet) => tweet.id));
772
- if (!known.has(action.tweetId)) {
773
- return {
774
- allowed: false,
775
- reason: `Tweet ${action.tweetId} is not in current observations. Choose a visible timeline/mention tweet for grounded engagement.`
776
- };
777
- }
778
- const targetTweet = tweetById.get(action.tweetId);
779
- if (targetTweet && (self && normalizeHandle(targetTweet.authorHandle) === self || selfId && targetTweet.authorId === selfId)) {
780
- return {
781
- allowed: false,
782
- reason: "Rejected self-interaction. Do not reply/like/retweet your own tweets."
783
- };
784
- }
785
- }
786
- if (action.action === "reply" && action.tweetId) {
787
- const inStrictTargetMode = strictReplyHandles.size > 0 || strictInteractHandles.size > 0;
788
- const repliesThisHeartbeat = executedActions.filter((a) => a.action === "reply").length;
789
- if (repliesThisHeartbeat >= 2 && !inStrictTargetMode) {
790
- return {
791
- allowed: false,
792
- reason: "Reply cap reached for this heartbeat. Use another tool action."
793
- };
794
- }
795
- const currentAuthor = normalizeHandle(tweetById.get(action.tweetId)?.authorHandle);
796
- if (currentAuthor) {
797
- const repliedAuthors = new Set(
798
- executedActions.filter((a) => a.action === "reply" && a.tweetId).map((a) => normalizeHandle(tweetById.get(a.tweetId)?.authorHandle)).filter(Boolean)
799
- );
800
- if (repliedAuthors.has(currentAuthor) && !inStrictTargetMode) {
801
- return {
802
- allowed: false,
803
- reason: `Already replied to @${currentAuthor} this heartbeat. Diversify interactions.`
804
- };
805
- }
806
- }
807
- if (hasRecentlyRepliedToTweet(action.tweetId)) {
808
- return {
809
- allowed: false,
810
- reason: `Already replied to tweet ${action.tweetId} recently. Engage a different conversation.`
811
- };
812
- }
813
- if (action.content) {
814
- const targetTweet = tweetById.get(action.tweetId);
815
- if (targetTweet && !hasMeaningfulContextOverlap(action.content, targetTweet.text)) {
816
- const phiHits = philosophicalPatternHits(action.content);
817
- if (wordCount(action.content) > 9 || phiHits > 0 || abstractWordCount(action.content) >= 4) {
818
- return {
819
- allowed: false,
820
- reason: "Reply is not grounded in the target tweet context. Reference concrete details from their actual post."
821
- };
822
- }
823
- }
824
- }
825
- }
826
- if (action.action === "follow" && action.handle) {
827
- const target = normalizeHandle(action.handle);
828
- if (!target) {
829
- return { allowed: false, reason: "Follow target is empty." };
830
- }
831
- if (self && target === self) {
832
- return {
833
- allowed: false,
834
- reason: "Rejected self-follow. Do not follow your own account."
835
- };
836
- }
837
- if (hasRecentlyFollowedHandle(target)) {
838
- return {
839
- allowed: false,
840
- reason: `Already followed @${target} recently. Choose a different action.`
841
- };
842
- }
843
- }
844
- return { allowed: true };
845
- }
846
-
847
- // src/memory/agent-network.ts
848
- import { existsSync, readFileSync, writeFileSync } from "fs";
849
- var SEED_HANDLES = ["sporaai"];
850
- function nowIso() {
851
- return (/* @__PURE__ */ new Date()).toISOString();
852
- }
853
- function normalizeHandle2(handle) {
854
- return handle.replace(/^@/, "").trim().toLowerCase();
855
- }
856
- function canonicalAgentHandle(handle) {
857
- const clean = normalizeHandle2(handle);
858
- if (clean === "spora") return "sporaai";
859
- return clean;
860
- }
861
- function likelyNetworkAgentHandle(handle) {
862
- const clean = canonicalAgentHandle(handle);
863
- if (!clean) return false;
864
- if (clean === "sporaai") return true;
865
- if (clean.includes("spora")) return true;
866
- if (clean.endsWith("_ai")) return true;
867
- if (clean.startsWith("giga")) return true;
868
- return false;
869
- }
870
- function defaultData() {
871
- const now = nowIso();
872
- return {
873
- version: 1,
874
- updatedAt: now,
875
- agents: SEED_HANDLES.map((handle) => ({
876
- handle,
877
- source: "seed",
878
- addedAt: now,
879
- lastSeenAt: now,
880
- seenCount: 1,
881
- active: true,
882
- notes: ["default seed"]
883
- }))
884
- };
885
- }
886
- function loadData() {
887
- if (!existsSync(paths.agentNetwork)) return defaultData();
888
- try {
889
- const parsed = JSON.parse(readFileSync(paths.agentNetwork, "utf-8"));
890
- const agents = Array.isArray(parsed.agents) ? parsed.agents : [];
891
- return {
892
- version: typeof parsed.version === "number" ? parsed.version : 1,
893
- updatedAt: typeof parsed.updatedAt === "string" ? parsed.updatedAt : nowIso(),
894
- agents: agents.map((entry) => {
895
- const handle = canonicalAgentHandle(String(entry.handle ?? ""));
896
- if (!handle) return null;
897
- return {
898
- handle,
899
- source: entry.source ?? "observed",
900
- addedAt: typeof entry.addedAt === "string" ? entry.addedAt : nowIso(),
901
- lastSeenAt: typeof entry.lastSeenAt === "string" ? entry.lastSeenAt : nowIso(),
902
- seenCount: typeof entry.seenCount === "number" ? Math.max(1, entry.seenCount) : 1,
903
- active: entry.active !== false,
904
- notes: Array.isArray(entry.notes) ? entry.notes.map((note) => String(note)) : []
905
- };
906
- }).filter((entry) => Boolean(entry))
907
- };
908
- } catch {
909
- return defaultData();
910
- }
911
- }
912
- function saveData(data) {
913
- const now = nowIso();
914
- const byHandle = /* @__PURE__ */ new Map();
915
- for (const entry of data.agents) {
916
- const handle = canonicalAgentHandle(entry.handle);
917
- if (!handle) continue;
918
- const existing = byHandle.get(handle);
919
- if (!existing) {
920
- byHandle.set(handle, {
921
- ...entry,
922
- handle
923
- });
924
- continue;
925
- }
926
- const addedAt = Date.parse(existing.addedAt) <= Date.parse(entry.addedAt) ? existing.addedAt : entry.addedAt;
927
- const lastSeenAt = Date.parse(existing.lastSeenAt) >= Date.parse(entry.lastSeenAt) ? existing.lastSeenAt : entry.lastSeenAt;
928
- byHandle.set(handle, {
929
- handle,
930
- source: existing.source === "seed" ? "seed" : entry.source,
931
- addedAt,
932
- lastSeenAt,
933
- seenCount: Math.max(existing.seenCount, entry.seenCount),
934
- active: existing.active || entry.active,
935
- notes: [.../* @__PURE__ */ new Set([...existing.notes, ...entry.notes])].slice(-8)
936
- });
937
- }
938
- if (!byHandle.has("sporaai")) {
939
- byHandle.set("sporaai", {
940
- handle: "sporaai",
941
- source: "seed",
942
- addedAt: now,
943
- lastSeenAt: now,
944
- seenCount: 1,
945
- active: true,
946
- notes: ["default seed"]
947
- });
948
- }
949
- const agents = [...byHandle.values()].filter((entry) => entry.active).sort((a, b) => {
950
- if (a.handle === "sporaai") return -1;
951
- if (b.handle === "sporaai") return 1;
952
- const bySeen = Date.parse(b.lastSeenAt) - Date.parse(a.lastSeenAt);
953
- if (!Number.isNaN(bySeen) && bySeen !== 0) return bySeen;
954
- return b.seenCount - a.seenCount;
955
- }).slice(0, 200);
956
- const next = {
957
- version: 1,
958
- updatedAt: now,
959
- agents
960
- };
961
- writeFileSync(paths.agentNetwork, JSON.stringify(next, null, 2));
962
- }
963
- function upsertHandleInternal(data, handle, source, note) {
964
- const clean = canonicalAgentHandle(handle);
965
- if (!clean) return false;
966
- if (!likelyNetworkAgentHandle(clean) && source !== "seed" && source !== "manual") return false;
967
- const now = nowIso();
968
- const existing = data.agents.find((entry) => canonicalAgentHandle(entry.handle) === clean);
969
- if (!existing) {
970
- data.agents.push({
971
- handle: clean,
972
- source,
973
- addedAt: now,
974
- lastSeenAt: now,
975
- seenCount: 1,
976
- active: true,
977
- notes: note ? [note] : []
978
- });
979
- return true;
980
- }
981
- existing.lastSeenAt = now;
982
- existing.seenCount += 1;
983
- existing.active = true;
984
- if (existing.source !== "seed") {
985
- existing.source = source;
986
- }
987
- if (note) {
988
- existing.notes = [.../* @__PURE__ */ new Set([...existing.notes, note])].slice(-8);
989
- }
990
- return false;
991
- }
992
- function upsertAgentHandle(handle, source, note) {
993
- const data = loadData();
994
- upsertHandleInternal(data, handle, source, note);
995
- saveData(data);
996
- }
997
- function upsertAgentHandles(handles, source, note) {
998
- if (handles.length === 0) return;
999
- const data = loadData();
1000
- for (const handle of handles) {
1001
- upsertHandleInternal(data, handle, source, note);
1002
- }
1003
- saveData(data);
1004
- }
1005
- function listAgentNetworkHandles(limit = 20) {
1006
- const data = loadData();
1007
- saveData(data);
1008
- return data.agents.filter((entry) => entry.active).slice(0, limit).map((entry) => canonicalAgentHandle(entry.handle));
1009
- }
1010
-
1011
- // src/runtime/research.ts
1012
- var handleToIdCache = /* @__PURE__ */ new Map();
1013
- var topicQueryHistory = [];
1014
- var peopleMonitorHistory = [];
1015
- function shuffle(items) {
1016
- const arr = [...items];
1017
- for (let i = arr.length - 1; i > 0; i -= 1) {
1018
- const j = Math.floor(Math.random() * (i + 1));
1019
- const tmp = arr[i];
1020
- arr[i] = arr[j];
1021
- arr[j] = tmp;
1022
- }
1023
- return arr;
1024
- }
1025
- function clampInt(value, min, max) {
1026
- return Math.max(min, Math.min(max, Math.round(value)));
1027
- }
1028
- function buildTopicQueryVariants(topic) {
1029
- const trimmed = topic.trim();
1030
- if (!trimmed) return [];
1031
- return [
1032
- trimmed,
1033
- `${trimmed} take`,
1034
- `${trimmed} question`,
1035
- `${trimmed} experience`,
1036
- `"${trimmed}" lang:en -is:retweet`
1037
- ];
1038
- }
1039
- function splitTopicSignal(signal) {
1040
- const normalized = signal.trim().toLowerCase().replace(/[^a-z0-9\s-]/g, " ").replace(/\s+/g, " ").trim();
1041
- if (!normalized) return [];
1042
- const words = normalized.split(" ");
1043
- if (words.length <= 6) return [normalized];
1044
- const compact = words.slice(0, 5).join(" ");
1045
- const keywords = words.filter((word) => word.length >= 5).slice(0, 3);
1046
- return [compact, ...keywords];
1047
- }
1048
- function buildTargetedPersonQueries(handles, heartbeatCount, budget) {
1049
- if (handles.length === 0 || budget <= 0) return [];
1050
- const start = heartbeatCount % handles.length;
1051
- const selected = [];
1052
- for (let i = 0; i < Math.min(budget, handles.length); i += 1) {
1053
- const handle = handles[(start + i) % handles.length];
1054
- selected.push(`from:${handle} -is:retweet`);
1055
- }
1056
- return selected;
1057
- }
1058
- function isLikelySyntheticHandle(handle) {
1059
- const clean = canonicalAgentHandle(handle);
1060
- return clean.endsWith("_ai") || clean.endsWith("bot");
1061
- }
1062
- function extractMentionHandles(text) {
1063
- return [...text.matchAll(/@([a-zA-Z0-9_]{1,15})/g)].map((match) => canonicalAgentHandle(match[1])).filter(Boolean);
1064
- }
1065
- var STOPWORDS = /* @__PURE__ */ new Set([
1066
- "about",
1067
- "after",
1068
- "again",
1069
- "against",
1070
- "among",
1071
- "because",
1072
- "being",
1073
- "between",
1074
- "could",
1075
- "every",
1076
- "first",
1077
- "found",
1078
- "from",
1079
- "going",
1080
- "have",
1081
- "having",
1082
- "here",
1083
- "into",
1084
- "just",
1085
- "like",
1086
- "make",
1087
- "more",
1088
- "most",
1089
- "much",
1090
- "only",
1091
- "other",
1092
- "over",
1093
- "really",
1094
- "same",
1095
- "some",
1096
- "such",
1097
- "than",
1098
- "that",
1099
- "their",
1100
- "there",
1101
- "these",
1102
- "they",
1103
- "this",
1104
- "those",
1105
- "through",
1106
- "time",
1107
- "very",
1108
- "what",
1109
- "when",
1110
- "where",
1111
- "which",
1112
- "while",
1113
- "with",
1114
- "would",
1115
- "your",
1116
- "youre",
1117
- "im",
1118
- "its",
1119
- "cant",
1120
- "dont",
1121
- "will",
1122
- "shall",
1123
- "should",
1124
- "also",
1125
- "twitter",
1126
- "tweet",
1127
- "tweets",
1128
- "thread",
1129
- "threads",
1130
- "http",
1131
- "https",
1132
- "retweet"
1133
- ]);
1134
- function discoverQueriesFromTweets(tweets, budget) {
1135
- const counts = /* @__PURE__ */ new Map();
1136
- for (const tweet of tweets) {
1137
- const tokens = tweet.text.toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[@#]\w+/g, " ").replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
1138
- for (const token of tokens) {
1139
- if (token.length < 5) continue;
1140
- if (/^\d+$/.test(token)) continue;
1141
- if (STOPWORDS.has(token)) continue;
1142
- counts.set(token, (counts.get(token) ?? 0) + 1);
1143
- }
1144
- }
1145
- return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, budget).map(([token]) => `"${token}" lang:en -is:retweet`);
1146
- }
1147
- function discoverPeopleFromTweets(tweets, selfHandle, budget) {
1148
- const scores = /* @__PURE__ */ new Map();
1149
- for (const tweet of tweets) {
1150
- const handle = tweet.authorHandle.replace(/^@/, "").toLowerCase();
1151
- if (!handle || handle === "unknown" || handle === selfHandle) continue;
1152
- let score = 1;
1153
- score += Math.min((tweet.replyCount ?? 0) / 15, 1.2);
1154
- score += Math.min((tweet.likeCount ?? 0) / 220, 0.9);
1155
- score += Math.min((tweet.retweetCount ?? 0) / 120, 0.8);
1156
- if (tweet.text.includes("?")) score += 0.25;
1157
- const existing = scores.get(handle) ?? 0;
1158
- if (score > existing) scores.set(handle, score);
1159
- }
1160
- return [...scores.entries()].sort((a, b) => b[1] - a[1]).slice(0, budget).map(([handle]) => handle);
1161
- }
1162
- function pickTopicQueries(allTopics, budget) {
1163
- const recent = new Set(topicQueryHistory.slice(-10));
1164
- const candidates = shuffle(allTopics.flatMap(buildTopicQueryVariants));
1165
- const picked = [];
1166
- for (const query of candidates) {
1167
- const normalized = query.toLowerCase();
1168
- if (recent.has(normalized)) continue;
1169
- picked.push(query);
1170
- if (picked.length >= budget) break;
1171
- }
1172
- if (picked.length < budget) {
1173
- for (const query of candidates) {
1174
- picked.push(query);
1175
- if (picked.length >= budget) break;
1176
- }
1177
- }
1178
- for (const query of picked) {
1179
- topicQueryHistory.push(query.toLowerCase());
1180
- }
1181
- if (topicQueryHistory.length > 100) {
1182
- topicQueryHistory.splice(0, topicQueryHistory.length - 100);
1183
- }
1184
- return picked;
1185
- }
1186
- function pickDirectQueries(queries, budget) {
1187
- const recent = new Set(topicQueryHistory.slice(-10));
1188
- const candidates = shuffle(queries);
1189
- const picked = [];
1190
- for (const query of candidates) {
1191
- const normalized = query.toLowerCase();
1192
- if (recent.has(normalized)) continue;
1193
- picked.push(query);
1194
- if (picked.length >= budget) break;
1195
- }
1196
- if (picked.length < budget) {
1197
- for (const query of candidates) {
1198
- if (picked.includes(query)) continue;
1199
- picked.push(query);
1200
- if (picked.length >= budget) break;
1201
- }
1202
- }
1203
- for (const query of picked) {
1204
- topicQueryHistory.push(query.toLowerCase());
1205
- }
1206
- if (topicQueryHistory.length > 100) {
1207
- topicQueryHistory.splice(0, topicQueryHistory.length - 100);
1208
- }
1209
- return picked;
1210
- }
1211
- async function resolveHandleToId(client, handle) {
1212
- const clean = handle.replace(/^@/, "");
1213
- if (handleToIdCache.has(clean)) {
1214
- return handleToIdCache.get(clean);
1215
- }
1216
- try {
1217
- const profile = await client.getProfile(clean);
1218
- handleToIdCache.set(clean, profile.id);
1219
- return profile.id;
1220
- } catch {
1221
- logger.warn(`Could not resolve handle @${clean} to user ID`);
1222
- return null;
1223
- }
1224
- }
1225
- async function runResearchPhase(client, heartbeatCount) {
1226
- const context = {
1227
- timeline: [],
1228
- mentions: [],
1229
- topicSearchResults: [],
1230
- peopleActivity: [],
1231
- ownPostPerformance: []
1232
- };
1233
- const [timeline, mentions] = await Promise.all([
1234
- client.getTimeline({ count: 20 }).catch(() => []),
1235
- client.getMentions({ count: 10 }).catch(() => [])
1236
- ]);
1237
- context.timeline = timeline;
1238
- context.mentions = mentions;
1239
- context.topicSearchResults = await runTopicSearch(client, heartbeatCount, [...timeline, ...mentions]);
1240
- context.peopleActivity = await runPeopleMonitoring(client, heartbeatCount, [...timeline, ...mentions]);
1241
- if (heartbeatCount % 2 === 0) {
1242
- context.ownPostPerformance = refreshOwnPostPerformance();
1243
- }
1244
- return context;
1245
- }
1246
- async function runTopicSearch(client, heartbeatCount, seedTweets) {
1247
- try {
1248
- const identity = loadIdentity();
1249
- const constraints = getPersonaConstraints(identity);
1250
- const strictHandles = personaConstraintHandles(constraints).filter(Boolean);
1251
- const strategy = loadStrategy();
1252
- const personaProfile = compilePersonaActionProfile({ identity, strategy, constraints });
1253
- const selfHandle = canonicalAgentHandle(identity.handle);
1254
- const topicSearchBias = personaProfile.sourceBias.topic_search;
1255
- const peopleWatchBias = personaProfile.sourceBias.people_watch;
1256
- const allTopics = [
1257
- ...personaProfile.priorityTopics,
1258
- ...identity.topics ?? [],
1259
- ...strategy.currentFocus
1260
- ].flatMap(splitTopicSignal).filter(Boolean);
1261
- const discoveredBudget = clampInt(1 + Math.max(0, topicSearchBias), 1, 3);
1262
- const discovered = discoverQueriesFromTweets(seedTweets, discoveredBudget);
1263
- const targetHandles = [.../* @__PURE__ */ new Set([
1264
- ...strategy.peopleToEngage.filter((person) => person.priority === "high" || person.priority === "medium").map((person) => person.handle.replace(/^@/, "").toLowerCase()),
1265
- ...(identity.heroes ?? []).map((handle) => handle.replace(/^@/, "").toLowerCase())
1266
- ])].filter((handle) => handle && handle !== selfHandle);
1267
- const strictQueries = buildTargetedPersonQueries(
1268
- strictHandles.filter((handle) => handle !== selfHandle),
1269
- heartbeatCount,
1270
- clampInt(2 + Math.max(0, topicSearchBias), 1, 4)
1271
- );
1272
- const personQueries = buildTargetedPersonQueries(
1273
- targetHandles,
1274
- heartbeatCount,
1275
- clampInt(1 + Math.max(0, peopleWatchBias), 0, 3)
1276
- );
1277
- const topicBudget = clampInt(3 + topicSearchBias, 1, 5);
1278
- const topicQueries = allTopics.length > 0 ? pickTopicQueries(allTopics, Math.min(topicBudget, Math.max(1, allTopics.length))) : [];
1279
- const discoveredQueries = discovered.length > 0 ? pickDirectQueries(discovered, Math.min(discoveredBudget, discovered.length)) : [];
1280
- const topicsToSearch = strictQueries.length > 0 ? [...new Set(strictQueries)] : [.../* @__PURE__ */ new Set([...topicQueries, ...discoveredQueries, ...personQueries])];
1281
- if (topicsToSearch.length === 0) return [];
1282
- const results = [];
1283
- for (const topicQuery of topicsToSearch) {
1284
- try {
1285
- const tweets = (await client.searchTweets(topicQuery, { count: 10 })).filter(
1286
- (tweet) => tweet.authorHandle.replace(/^@/, "").toLowerCase() !== selfHandle
1287
- );
1288
- if (tweets.length > 0) {
1289
- results.push({ query: topicQuery, tweets });
1290
- logger.info(`Topic search "${topicQuery}": found ${tweets.length} tweets`);
1291
- }
1292
- } catch {
1293
- }
1294
- }
1295
- return results;
1296
- } catch {
1297
- return [];
1298
- }
1299
- }
1300
- async function runPeopleMonitoring(client, heartbeatCount, seedTweets) {
1301
- try {
1302
- const strategy = loadStrategy();
1303
- const identity = loadIdentity();
1304
- const constraints = getPersonaConstraints(identity);
1305
- const personaProfile = compilePersonaActionProfile({ identity, strategy, constraints });
1306
- const strictHandles = personaConstraintHandles(constraints).map((handle) => canonicalAgentHandle(handle)).filter((handle) => handle.length > 0);
1307
- const selfHandle = identity.handle.replace(/^@/, "").toLowerCase();
1308
- const relationships = loadRelationships();
1309
- const networkHandles = listAgentNetworkHandles(20);
1310
- const networkHandleSet = new Set(networkHandles);
1311
- if (strictHandles.length > 0) {
1312
- const strictResults = [];
1313
- for (const handle of strictHandles) {
1314
- if (handle === selfHandle) continue;
1315
- const userId = await resolveHandleToId(client, handle);
1316
- if (!userId) continue;
1317
- try {
1318
- const tweets = await client.getUserTweets(userId, { count: 8 });
1319
- if (tweets.length === 0) continue;
1320
- strictResults.push({
1321
- handle,
1322
- userId,
1323
- reason: "persona hard constraint",
1324
- tweets
1325
- });
1326
- upsertAgentHandle(handle, "observed", "people check (strict persona target)");
1327
- logger.info(`People check @${handle}: ${tweets.length} recent tweets (strict target mode)`);
1328
- } catch {
1329
- }
1330
- }
1331
- return strictResults;
1332
- }
1333
- const people = [];
1334
- const seen = /* @__PURE__ */ new Set();
1335
- const highPriorityHandles = new Set(
1336
- strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => person.handle.replace(/^@/, "").toLowerCase())
1337
- );
1338
- const protectedHandles = /* @__PURE__ */ new Set([
1339
- ...highPriorityHandles,
1340
- ...(identity.heroes ?? []).map((handle) => handle.replace(/^@/, "").toLowerCase()),
1341
- ...networkHandleSet
1342
- ]);
1343
- const addPerson = (handle, reason, pinned2 = false) => {
1344
- const clean = canonicalAgentHandle(handle);
1345
- if (seen.has(clean)) return;
1346
- if (clean === selfHandle) return;
1347
- if (isLikelySyntheticHandle(clean) && !protectedHandles.has(clean)) return;
1348
- seen.add(clean);
1349
- people.push({ handle: clean, reason, pinned: pinned2 });
1350
- };
1351
- for (const p of strategy.peopleToEngage.filter((p2) => p2.priority === "high")) {
1352
- addPerson(p.handle, p.reason, true);
1353
- }
1354
- const sporaPinInterval = personaProfile.sourceBias.people_watch >= 0.6 ? 4 : 8;
1355
- for (const handle of networkHandles) {
1356
- if (handle === "sporaai") {
1357
- const pinSpora = heartbeatCount % sporaPinInterval === 0;
1358
- addPerson(handle, "core Spora profile", pinSpora);
1359
- } else {
1360
- addPerson(handle, "spora agent network");
1361
- }
1362
- }
1363
- for (const hero of identity.heroes ?? []) {
1364
- addPerson(hero, "hero/inspiration");
1365
- }
1366
- const sporeRels = Object.values(relationships.accounts).filter((rel) => rel.isSpore).sort((a, b) => b.interactionCount - a.interactionCount).slice(0, 5);
1367
- for (const rel of sporeRels) {
1368
- addPerson(rel.handle, "spora relationship");
1369
- upsertAgentHandle(rel.handle, "relationship", "relationship marked isSpore");
1370
- }
1371
- const topRels = Object.values(relationships.accounts).sort((a, b) => b.interactionCount - a.interactionCount).slice(0, 5);
1372
- for (const r of topRels) {
1373
- addPerson(r.handle, "frequent interactor");
1374
- }
1375
- for (const p of strategy.peopleToEngage.filter((p2) => p2.priority === "medium")) {
1376
- addPerson(p.handle, p.reason);
1377
- }
1378
- for (const handle of discoverPeopleFromTweets(seedTweets, selfHandle, 6)) {
1379
- addPerson(handle, "active voice in current conversations");
1380
- }
1381
- if (people.length === 0) return [];
1382
- const monitoringBudget = clampInt(3 + personaProfile.sourceBias.people_watch, 2, 5);
1383
- const budget = Math.min(monitoringBudget, people.length);
1384
- const selected = [];
1385
- const selectedHandles = /* @__PURE__ */ new Set();
1386
- const pinned = people.filter((person) => person.pinned);
1387
- for (const person of pinned) {
1388
- if (selected.length >= budget) break;
1389
- selected.push(person);
1390
- selectedHandles.add(person.handle);
1391
- }
1392
- if (selected.length < budget) {
1393
- const rest = people.filter((person) => !selectedHandles.has(person.handle));
1394
- if (rest.length > 0) {
1395
- const startIdx = heartbeatCount * 2 % rest.length;
1396
- const rotated = [...rest.slice(startIdx), ...rest.slice(0, startIdx)];
1397
- const recentlyChecked = new Set(peopleMonitorHistory.slice(-8));
1398
- const preferred = rotated.filter((person) => !recentlyChecked.has(person.handle));
1399
- const pool = preferred.length >= budget - selected.length ? preferred : rotated;
1400
- for (const person of pool) {
1401
- if (selected.length >= budget) break;
1402
- selected.push(person);
1403
- }
1404
- }
1405
- }
1406
- for (const person of selected) {
1407
- peopleMonitorHistory.push(person.handle);
1408
- }
1409
- if (peopleMonitorHistory.length > 120) {
1410
- peopleMonitorHistory.splice(0, peopleMonitorHistory.length - 120);
1411
- }
1412
- const results = [];
1413
- const discoveredHandles = /* @__PURE__ */ new Set();
1414
- for (const person of selected) {
1415
- const userId = await resolveHandleToId(client, person.handle);
1416
- if (!userId) continue;
1417
- try {
1418
- const tweets = await client.getUserTweets(userId, { count: 5 });
1419
- if (tweets.length > 0) {
1420
- results.push({
1421
- handle: person.handle,
1422
- userId,
1423
- reason: person.reason,
1424
- tweets
1425
- });
1426
- upsertAgentHandle(person.handle, "observed", "people check");
1427
- discoveredHandles.add(canonicalAgentHandle(person.handle));
1428
- for (const tweet of tweets) {
1429
- for (const mentionHandle of extractMentionHandles(tweet.text)) {
1430
- discoveredHandles.add(canonicalAgentHandle(mentionHandle));
1431
- }
1432
- }
1433
- logger.info(`People check @${person.handle}: ${tweets.length} recent tweets`);
1434
- }
1435
- } catch {
1436
- }
1437
- }
1438
- if (discoveredHandles.size > 0) {
1439
- upsertAgentHandles([...discoveredHandles], "observed", "discovered via people monitoring");
1440
- }
1441
- return results;
1442
- } catch {
1443
- return [];
1444
- }
1445
- }
1446
- function refreshOwnPostPerformance() {
1447
- try {
1448
- retireOldPosts();
1449
- return getActiveTrackedPosts().slice(-5);
1450
- } catch {
1451
- return [];
1452
- }
1453
- }
1454
-
1455
- // src/runtime/opportunity-engine.ts
1456
- function normalizeHandle3(handle) {
1457
- return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
1458
- }
1459
- function isLikelySyntheticHandle2(handle) {
1460
- const clean = normalizeHandle3(handle);
1461
- return clean === "spora" || clean.endsWith("_ai") || clean.endsWith("bot");
1462
- }
1463
- function normalizeText(text) {
1464
- return text.toLowerCase().replace(/https?:\/\/\S+/g, " ").replace(/[@#]\w+/g, " ").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
1465
- }
1466
- function clip(text, max = 190) {
1467
- if (text.length <= max) return text;
1468
- return `${text.slice(0, max - 3)}...`;
1469
- }
1470
- function splitKeywords(text) {
1471
- const normalized = normalizeText(text);
1472
- if (!normalized) return [];
1473
- const words = normalized.split(" ");
1474
- if (words.length <= 6) return [normalized];
1475
- const concise = words.filter((word) => word.length >= 4).slice(0, 6);
1476
- return concise.length > 0 ? concise : [words.slice(0, 6).join(" ")];
1477
- }
1478
- function unique(values) {
1479
- return [...new Set(values)];
1480
- }
1481
- function buildPersonaContext() {
1482
- const identity = loadIdentity();
1483
- const constraints = getPersonaConstraints(identity);
1484
- const strategy = loadStrategy();
1485
- const actionProfile = compilePersonaActionProfile({ identity, strategy, constraints });
1486
- const focusKeywords = unique(
1487
- [
1488
- ...identity.topics,
1489
- ...identity.coreValues,
1490
- ...identity.goals,
1491
- ...strategy.currentFocus,
1492
- ...strategy.shortTermGoals
1493
- ].flatMap(splitKeywords).map((keyword) => keyword.trim()).filter((keyword) => keyword.length >= 3).slice(0, 40)
1494
- );
1495
- const avoidKeywords = unique(
1496
- identity.avoidTopics.flatMap(splitKeywords).map((keyword) => keyword.trim()).filter((keyword) => keyword.length >= 3).slice(0, 20)
1497
- );
1498
- const priorityHandles = new Set(
1499
- strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => normalizeHandle3(person.handle)).filter(Boolean)
1500
- );
1501
- for (const handle of personaConstraintHandles(constraints)) {
1502
- priorityHandles.add(normalizeHandle3(handle));
1503
- }
1504
- const heroHandles = new Set(
1505
- (identity.heroes ?? []).map((handle) => normalizeHandle3(handle)).filter(Boolean)
1506
- );
1507
- const networkHandles = new Set(listAgentNetworkHandles(60).map((handle) => normalizeHandle3(handle)).filter(Boolean));
1508
- return { focusKeywords, avoidKeywords, priorityHandles, heroHandles, networkHandles, actionProfile };
1509
- }
1510
- function personaActionDelta(actionType, source, profile) {
1511
- return profile.actionBias[actionType] * 0.9 + profile.sourceBias[source] * 0.55;
1512
- }
1513
- function isActionStronglySuppressed(actionType, profile) {
1514
- return profile.actionBias[actionType] <= -1.35;
1515
- }
1516
- function matchesPersonaGeneratedProfile(actionType, source, profile) {
1517
- const actionBias = profile.actionBias[actionType];
1518
- const sourceBias = profile.sourceBias[source];
1519
- const combined = personaActionDelta(actionType, source, profile);
1520
- if (actionBias <= -0.55) return false;
1521
- if (sourceBias <= -1.05) return false;
1522
- if (combined <= -0.3) return false;
1523
- return true;
1524
- }
1525
- function phraseMatchScore(text, keywords) {
1526
- if (keywords.length === 0) return 0;
1527
- const normalized = normalizeText(text);
1528
- if (!normalized) return 0;
1529
- let score = 0;
1530
- for (const keyword of keywords) {
1531
- const needle = normalizeText(keyword);
1532
- if (!needle) continue;
1533
- if (!normalized.includes(needle)) continue;
1534
- const wordCount2 = needle.split(" ").length;
1535
- score += wordCount2 >= 2 ? 0.7 : 0.35;
1536
- }
1537
- return Math.min(score, 2.4);
1538
- }
1539
- function recencyPenalty(lastTouchedAt) {
1540
- if (!lastTouchedAt) return 0;
1541
- const ageHours = (Date.now() - lastTouchedAt) / (1e3 * 60 * 60);
1542
- if (ageHours < 1) return 2;
1543
- if (ageHours < 4) return 1.1;
1544
- if (ageHours < 12) return 0.55;
1545
- return 0;
1546
- }
1547
- function buildRecentIndexes(recent) {
1548
- const repliedTweetIds = /* @__PURE__ */ new Set();
1549
- const likedTweetIds = /* @__PURE__ */ new Set();
1550
- const retweetedTweetIds = /* @__PURE__ */ new Set();
1551
- const followedHandles = /* @__PURE__ */ new Set();
1552
- const authorTouchCount = /* @__PURE__ */ new Map();
1553
- const authorLastTouchedAt = /* @__PURE__ */ new Map();
1554
- for (const entry of recent) {
1555
- if (entry.type === "reply" && entry.inReplyTo) {
1556
- repliedTweetIds.add(entry.inReplyTo);
1557
- }
1558
- if (entry.type === "like" && entry.tweetId) {
1559
- likedTweetIds.add(entry.tweetId);
1560
- }
1561
- if (entry.type === "retweet" && entry.tweetId) {
1562
- retweetedTweetIds.add(entry.tweetId);
1563
- }
1564
- if (entry.type === "follow" && entry.targetHandle) {
1565
- followedHandles.add(normalizeHandle3(entry.targetHandle));
1566
- }
1567
- if (entry.targetHandle) {
1568
- const handle = normalizeHandle3(entry.targetHandle);
1569
- if (!handle) continue;
1570
- authorTouchCount.set(handle, (authorTouchCount.get(handle) ?? 0) + 1);
1571
- const ts = Date.parse(entry.timestamp);
1572
- if (!Number.isNaN(ts)) {
1573
- const existing = authorLastTouchedAt.get(handle) ?? 0;
1574
- if (ts > existing) authorLastTouchedAt.set(handle, ts);
1575
- }
1576
- }
1577
- }
1578
- return {
1579
- repliedTweetIds,
1580
- likedTweetIds,
1581
- retweetedTweetIds,
1582
- followedHandles,
1583
- authorTouchCount,
1584
- authorLastTouchedAt
1585
- };
1586
- }
1587
- function sourceWeight(source) {
1588
- if (source === "mention") return 3.2;
1589
- if (source === "people_watch") return 2.4;
1590
- if (source === "topic_search") return 2;
1591
- if (source === "timeline") return 1.5;
1592
- return 1;
1593
- }
1594
- function armKey(actionType, source) {
1595
- return `${actionType}:${source}`;
1596
- }
1597
- function scoreTweetOpportunity(source, tweet, authorTouchCount, authorLastTouchedAt, recentlyReplied, persona) {
1598
- const handle = normalizeHandle3(tweet.authorHandle);
1599
- const priorTouches = authorTouchCount.get(handle) ?? 0;
1600
- const focusMatch = phraseMatchScore(tweet.text, persona.focusKeywords);
1601
- const avoidMatch = phraseMatchScore(tweet.text, persona.avoidKeywords);
1602
- const targetBoost = persona.priorityHandles.has(handle) ? 2.4 : persona.heroHandles.has(handle) ? 1.4 : persona.networkHandles.has(handle) ? 0.9 : 0;
1603
- const alignment = focusMatch + targetBoost - avoidMatch * 1.2;
1604
- let score = sourceWeight(source);
1605
- score += Math.min((tweet.replyCount ?? 0) / 12, 1.2);
1606
- score += Math.min((tweet.likeCount ?? 0) / 180, 1);
1607
- score += tweet.text.includes("?") ? 0.75 : 0;
1608
- score += alignment;
1609
- score += priorTouches === 0 ? 0.8 : Math.max(0.05, 0.55 - priorTouches * 0.14);
1610
- score -= recencyPenalty(authorLastTouchedAt.get(handle));
1611
- score -= recentlyReplied ? 3.5 : 0;
1612
- if (priorTouches >= 5) score -= 0.8;
1613
- if (avoidMatch > 0.6) score -= 1.2;
1614
- if (isLikelySyntheticHandle2(handle) && !persona.priorityHandles.has(handle) && !persona.heroHandles.has(handle) && !persona.networkHandles.has(handle)) {
1615
- score -= 2.4;
1616
- }
1617
- return { score, alignment };
1618
- }
1619
- function buildActionOpportunities(input) {
1620
- const { research } = input;
1621
- const maxCandidates = input.maxCandidates ?? 28;
1622
- const selfHandle = normalizeHandle3(input.selfHandle);
1623
- const selfUserId = (input.selfUserId ?? "").trim();
1624
- const constraints = getPersonaConstraints();
1625
- const onlyReplyHandles = new Set(constraints.onlyReplyToHandles.map((h) => normalizeHandle3(h)));
1626
- const onlyInteractHandles = new Set(constraints.onlyInteractWithHandles.map((h) => normalizeHandle3(h)));
1627
- const persona = buildPersonaContext();
1628
- const actionProfile = persona.actionProfile;
1629
- const recent = getRecentInteractions(300);
1630
- const {
1631
- repliedTweetIds,
1632
- likedTweetIds,
1633
- retweetedTweetIds,
1634
- followedHandles,
1635
- authorTouchCount,
1636
- authorLastTouchedAt
1637
- } = buildRecentIndexes(recent);
1638
- const candidateTweets = [];
1639
- for (const tweet of research.mentions) candidateTweets.push({ tweet, source: "mention" });
1640
- for (const tweet of research.timeline) candidateTweets.push({ tweet, source: "timeline" });
1641
- for (const result of research.topicSearchResults) {
1642
- for (const tweet of result.tweets) candidateTweets.push({ tweet, source: "topic_search" });
1643
- }
1644
- for (const person of research.peopleActivity) {
1645
- for (const tweet of person.tweets) candidateTweets.push({ tweet, source: "people_watch" });
1646
- }
1647
- const dedupedByTweetId = /* @__PURE__ */ new Map();
1648
- for (const candidate of candidateTweets) {
1649
- const handle = normalizeHandle3(candidate.tweet.authorHandle);
1650
- if (!candidate.tweet.id || !handle || handle === selfHandle) continue;
1651
- if (selfUserId && candidate.tweet.authorId === selfUserId) continue;
1652
- if (onlyInteractHandles.size > 0 && !onlyInteractHandles.has(handle)) continue;
1653
- const existing = dedupedByTweetId.get(candidate.tweet.id);
1654
- if (!existing || sourceWeight(candidate.source) > sourceWeight(existing.source)) {
1655
- dedupedByTweetId.set(candidate.tweet.id, candidate);
1656
- }
1657
- }
1658
- const followOpportunityByHandle = /* @__PURE__ */ new Set();
1659
- const opportunities = [];
1660
- let counter = 0;
1661
- const nextId = () => {
1662
- counter += 1;
1663
- return `opp-${counter}`;
1664
- };
1665
- for (const { tweet, source } of dedupedByTweetId.values()) {
1666
- const handle = normalizeHandle3(tweet.authorHandle);
1667
- const { score: baseScore, alignment } = scoreTweetOpportunity(
1668
- source,
1669
- tweet,
1670
- authorTouchCount,
1671
- authorLastTouchedAt,
1672
- repliedTweetIds.has(tweet.id),
1673
- persona
1674
- );
1675
- const shortTweet = clip(tweet.text, 170);
1676
- const replyScore = baseScore + 0.7 + personaActionDelta("reply", source, actionProfile);
1677
- const likeScore = baseScore + 0.2 + personaActionDelta("like", source, actionProfile);
1678
- const retweetScore = baseScore - 0.1 + personaActionDelta("retweet", source, actionProfile);
1679
- const followScore = baseScore - 0.2 + personaActionDelta("follow", source, actionProfile);
1680
- if (!isActionStronglySuppressed("reply", actionProfile) && matchesPersonaGeneratedProfile("reply", source, actionProfile)) {
1681
- opportunities.push({
1682
- id: nextId(),
1683
- armKey: armKey("reply", source),
1684
- actionType: "reply",
1685
- source,
1686
- score: replyScore,
1687
- summary: `Reply to @${handle} from ${source}`,
1688
- authorHandle: handle,
1689
- tweetId: tweet.id,
1690
- requiresContent: true,
1691
- template: { action: "reply", tweetId: tweet.id, source, targetHandle: `@${handle}` },
1692
- context: `@${handle}: "${shortTweet}"`
1693
- });
1694
- }
1695
- const stronglyRelevant = alignment >= 0.35 || source === "mention" || source === "people_watch" || persona.priorityHandles.has(handle);
1696
- if (onlyReplyHandles.size === 0 && !constraints.replyOnlyMode && !isActionStronglySuppressed("like", actionProfile) && matchesPersonaGeneratedProfile("like", source, actionProfile) && !likedTweetIds.has(tweet.id) && likeScore >= 1.8 && stronglyRelevant) {
1697
- opportunities.push({
1698
- id: nextId(),
1699
- armKey: armKey("like", source),
1700
- actionType: "like",
1701
- source,
1702
- score: likeScore,
1703
- summary: `Like @${handle} from ${source}`,
1704
- authorHandle: handle,
1705
- tweetId: tweet.id,
1706
- requiresContent: false,
1707
- template: { action: "like", tweetId: tweet.id, source, targetHandle: `@${handle}` },
1708
- context: `@${handle}: "${shortTweet}"`
1709
- });
1710
- }
1711
- if (onlyReplyHandles.size === 0 && !constraints.replyOnlyMode && !isActionStronglySuppressed("retweet", actionProfile) && matchesPersonaGeneratedProfile("retweet", source, actionProfile) && !retweetedTweetIds.has(tweet.id) && retweetScore >= 2.6 && alignment >= 0.15) {
1712
- opportunities.push({
1713
- id: nextId(),
1714
- armKey: armKey("retweet", source),
1715
- actionType: "retweet",
1716
- source,
1717
- score: retweetScore,
1718
- summary: `Retweet @${handle} from ${source}`,
1719
- authorHandle: handle,
1720
- tweetId: tweet.id,
1721
- requiresContent: false,
1722
- template: { action: "retweet", tweetId: tweet.id, source, targetHandle: `@${handle}` },
1723
- context: `@${handle}: "${shortTweet}"`
1724
- });
1725
- }
1726
- const shouldConsiderFollow = onlyReplyHandles.size === 0 && !constraints.replyOnlyMode && !isActionStronglySuppressed("follow", actionProfile) && matchesPersonaGeneratedProfile("follow", source, actionProfile) && !followedHandles.has(handle) && !followOpportunityByHandle.has(handle) && !isLikelySyntheticHandle2(handle) && followScore >= 3 && (persona.priorityHandles.has(handle) || persona.heroHandles.has(handle) || source === "mention" || alignment >= 0.8);
1727
- if (shouldConsiderFollow) {
1728
- followOpportunityByHandle.add(handle);
1729
- opportunities.push({
1730
- id: nextId(),
1731
- armKey: armKey("follow", source),
1732
- actionType: "follow",
1733
- source,
1734
- score: followScore,
1735
- summary: `Follow @${handle}`,
1736
- authorHandle: handle,
1737
- requiresContent: false,
1738
- template: { action: "follow", handle: `@${handle}`, source, targetHandle: `@${handle}` },
1739
- context: `Account @${handle} is producing conversation-relevant content.`
1740
- });
1741
- }
1742
- }
1743
- const queryList = research.topicSearchResults.map((r) => r.query).filter(Boolean);
1744
- if (queryList.length > 0 && !constraints.noOriginalPosts && !constraints.replyOnlyMode && !isActionStronglySuppressed("post", actionProfile) && matchesPersonaGeneratedProfile("post", "synthesis", actionProfile) && onlyInteractHandles.size === 0 && onlyReplyHandles.size === 0) {
1745
- const synthesisScore = 2.25 + personaActionDelta("post", "synthesis", actionProfile);
1746
- const priorityTopicHint = actionProfile.priorityTopics.slice(0, 3).join(", ");
1747
- opportunities.push({
1748
- id: nextId(),
1749
- armKey: armKey("post", "synthesis"),
1750
- actionType: "post",
1751
- source: "synthesis",
1752
- score: synthesisScore,
1753
- summary: "Post an original thought synthesized from active conversations",
1754
- requiresContent: true,
1755
- template: { action: "post", source: "synthesis" },
1756
- context: `Weave a fresh take from active topics: ${queryList.slice(0, 4).join(", ")}${priorityTopicHint ? `. Persona-priority topics: ${priorityTopicHint}` : ""}`
1757
- });
1758
- }
1759
- opportunities.sort((a, b) => b.score - a.score);
1760
- return opportunities.slice(0, maxCandidates);
1761
- }
1762
-
1763
- // src/runtime/portfolio-planner.ts
1764
- function parseSelections(responseText) {
1765
- const codeBlockMatch = responseText.match(/```(?:json)?\s*\n?([\s\S]*?)```/);
1766
- const candidatesToTry = [];
1767
- if (codeBlockMatch?.[1]) candidatesToTry.push(codeBlockMatch[1].trim());
1768
- candidatesToTry.push(responseText.trim());
1769
- for (const raw of candidatesToTry) {
1770
- try {
1771
- const parsed = JSON.parse(raw);
1772
- if (Array.isArray(parsed.selections)) return parsed.selections;
1773
- } catch {
1774
- }
1775
- }
1776
- const arrayMatch = responseText.match(/\[\s*\{[\s\S]*\}\s*\]/);
1777
- if (arrayMatch) {
1778
- try {
1779
- const parsed = JSON.parse(arrayMatch[0]);
1780
- if (Array.isArray(parsed)) return parsed;
1781
- } catch {
1782
- }
1783
- }
1784
- return [];
1785
- }
1786
- function materializeSelections(selections, opportunities, maxActions) {
1787
- const byId = new Map(opportunities.map((opportunity) => [opportunity.id, opportunity]));
1788
- const actions = [];
1789
- const used = /* @__PURE__ */ new Set();
1790
- for (const selection of selections) {
1791
- if (actions.length >= maxActions) break;
1792
- if (!selection.candidateId || used.has(selection.candidateId)) continue;
1793
- const opportunity = byId.get(selection.candidateId);
1794
- if (!opportunity) continue;
1795
- const action = {
1796
- ...opportunity.template,
1797
- banditArm: opportunity.armKey,
1798
- opportunityId: opportunity.id,
1799
- reasoning: selection.reasoning
1800
- };
1801
- if (opportunity.requiresContent) {
1802
- const content = (selection.content ?? "").trim();
1803
- if (!content) continue;
1804
- action.content = content;
1805
- }
1806
- used.add(selection.candidateId);
1807
- actions.push(action);
1808
- }
1809
- return actions;
1810
- }
1811
- async function planActionPortfolio(input) {
1812
- const { systemPrompt, opportunities, maxActions, policyFeedback, executedActions = [] } = input;
1813
- if (opportunities.length === 0 || maxActions <= 0) return [];
1814
- const prompt = buildOpportunityPortfolioMessage({
1815
- opportunities,
1816
- maxActions,
1817
- policyFeedback,
1818
- executedActions
1819
- });
1820
- const llmResponse = await generateResponse(systemPrompt, prompt);
1821
- const parsed = parseSelections(llmResponse.content);
1822
- const actions = materializeSelections(parsed, opportunities, maxActions);
1823
- if (actions.length === 0) {
1824
- logger.warn("Portfolio planner returned no valid selections.");
1825
- }
1826
- return actions;
1827
- }
1828
-
1829
- // src/runtime/mission-director.ts
1830
- function normalizeHandle4(handle) {
1831
- return handle.replace(/^@/, "").trim().toLowerCase();
1832
- }
1833
- function unique2(values) {
1834
- return [...new Set(values)];
1835
- }
1836
- function isAction(value) {
1837
- return ["reply", "like", "retweet", "follow", "post"].includes(value);
1838
- }
1839
- function sanitizeActions(raw, fallback) {
1840
- if (!Array.isArray(raw) || raw.length === 0) return fallback;
1841
- const cleaned = raw.map((item) => String(item).trim().toLowerCase()).filter((item) => isAction(item));
1842
- return cleaned.length > 0 ? unique2(cleaned) : fallback;
1843
- }
1844
- function parseJsonObject(text) {
1845
- const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
1846
- const candidates = [fenced, text].filter((v) => Boolean(v)).map((v) => v.trim());
1847
- for (const candidate of candidates) {
1848
- try {
1849
- return JSON.parse(candidate);
1850
- } catch {
1851
- }
1852
- const obj = candidate.match(/\{[\s\S]*\}/);
1853
- if (!obj) continue;
1854
- try {
1855
- return JSON.parse(obj[0]);
1856
- } catch {
1857
- }
1858
- }
1859
- return null;
1860
- }
1861
- function buildPersonaAllowedActions(profile, noOriginalPosts) {
1862
- const byBias = profile.prioritizedActions.filter((action) => profile.actionBias[action] > -1.1);
1863
- const withFallback = byBias.length > 0 ? byBias : ["reply", "like", "retweet", "follow", "post"];
1864
- const filtered = noOriginalPosts ? withFallback.filter((action) => action !== "post") : withFallback;
1865
- return unique2(filtered).slice(0, 5);
1866
- }
1867
- function buildDeterministicMission(research, profile) {
1868
- const identity = loadIdentity();
1869
- const strategy = loadStrategy();
1870
- const constraints = getPersonaConstraints(identity);
1871
- const constraintTargets = personaConstraintHandles(constraints).map(normalizeHandle4).filter(Boolean);
1872
- const focusTopics = unique2(
1873
- [
1874
- ...profile.priorityTopics,
1875
- ...identity.topics ?? [],
1876
- ...strategy.currentFocus,
1877
- ...strategy.shortTermGoals
1878
- ].map((topic) => String(topic).trim()).filter(Boolean).slice(0, 10)
1879
- );
1880
- const strictReply = constraints.onlyReplyToHandles.length > 0 || constraints.replyOnlyMode;
1881
- if (strictReply) {
1882
- const strictTargets = constraints.onlyReplyToHandles.length > 0 ? constraints.onlyReplyToHandles.map(normalizeHandle4) : constraintTargets;
1883
- return {
1884
- mode: "strict_target_reply",
1885
- objective: strictTargets.length > 0 ? `Reply only to @${strictTargets.join(", @")} while staying fully in character.` : "Reply-only mode. Stay in character and do not perform non-reply actions.",
1886
- allowedActions: ["reply"],
1887
- targetHandles: strictTargets,
1888
- focusTopics,
1889
- rationale: "Hard persona constraint detected in identity goals/boundaries/bio."
1890
- };
1891
- }
1892
- if (constraintTargets.length > 0) {
1893
- return {
1894
- mode: "strict_target_interaction",
1895
- objective: `Interact only with @${constraintTargets.join(", @")} in persona voice.`,
1896
- allowedActions: constraints.noOriginalPosts ? ["reply", "like", "retweet", "follow"] : ["reply", "like", "retweet", "follow", "post"],
1897
- targetHandles: constraintTargets,
1898
- focusTopics,
1899
- rationale: "Hard persona interaction target detected."
1900
- };
1901
- }
1902
- const preferredActions = buildPersonaAllowedActions(profile, constraints.noOriginalPosts);
1903
- const primaryActionHint = preferredActions.length > 0 ? preferredActions.slice(0, 2).join(" + ") : "context-aware interactions";
1904
- return {
1905
- mode: "persona_balanced",
1906
- objective: `Advance ${identity.name}'s mission through ${primaryActionHint} while staying in strict persona voice.`,
1907
- allowedActions: preferredActions,
1908
- targetHandles: [],
1909
- focusTopics,
1910
- rationale: `No strict target lock; persona profile drives soft action/source priorities (${profile.rationale.slice(0, 2).join("; ")}).`
1911
- };
1912
- }
1913
- function buildMissionPrompt(research, fallback, profile) {
1914
- const identity = loadIdentity();
1915
- const constraints = getPersonaConstraints(identity);
1916
- const topTimeline = research.timeline.slice(0, 5);
1917
- const topMentions = research.mentions.slice(0, 5);
1918
- const topicQueries = research.topicSearchResults.map((r) => r.query).filter(Boolean).slice(0, 6);
1919
- const people = research.peopleActivity.map((p) => p.handle).filter(Boolean).slice(0, 6);
1920
- const lines = [];
1921
- lines.push("Create one heartbeat mission JSON for this agent.");
1922
- lines.push("Hard rule: Persona constraints override everything.");
1923
- lines.push("");
1924
- lines.push(`Agent: ${identity.name} (@${identity.handle})`);
1925
- lines.push(`Bio: ${identity.bio}`);
1926
- lines.push(`Tone: ${identity.tone}`);
1927
- lines.push(`Goals: ${(identity.goals ?? []).join(" | ")}`);
1928
- lines.push(`Boundaries: ${(identity.boundaries ?? []).join(" | ")}`);
1929
- lines.push(`Framework: ${identity.framework}`);
1930
- lines.push(`Existing strict constraints: ${JSON.stringify(constraints)}`);
1931
- lines.push(`Persona action priors: ${JSON.stringify({
1932
- prioritizedActions: profile.prioritizedActions.slice(0, 5),
1933
- priorityTopics: profile.priorityTopics.slice(0, 8),
1934
- actionBias: profile.actionBias,
1935
- sourceBias: profile.sourceBias,
1936
- rationale: profile.rationale.slice(0, 5)
1937
- })}`);
1938
- lines.push("");
1939
- lines.push(`Fallback mission if unsure: ${JSON.stringify(fallback)}`);
1940
- lines.push("");
1941
- lines.push(`Observed timeline sample (${topTimeline.length}):`);
1942
- for (const tweet of topTimeline) {
1943
- lines.push(`- @${tweet.authorHandle}: ${tweet.text.slice(0, 180)}`);
1944
- }
1945
- lines.push(`Observed mentions sample (${topMentions.length}):`);
1946
- for (const tweet of topMentions) {
1947
- lines.push(`- @${tweet.authorHandle}: ${tweet.text.slice(0, 180)}`);
1948
- }
1949
- lines.push(`Topic queries: ${topicQueries.join(" | ") || "none"}`);
1950
- lines.push(`People watched: ${people.map((h) => `@${h}`).join(", ") || "none"}`);
1951
- lines.push("");
1952
- lines.push("Return JSON only with this shape:");
1953
- lines.push("{");
1954
- lines.push(' "mode": "strict_target_reply|strict_target_interaction|persona_balanced",');
1955
- lines.push(' "objective": "one-sentence mission",');
1956
- lines.push(' "allowedActions": ["reply","like","retweet","follow","post"],');
1957
- lines.push(' "targetHandles": ["grok"],');
1958
- lines.push(' "focusTopics": ["topic a","topic b"],');
1959
- lines.push(' "rationale": "short why"');
1960
- lines.push("}");
1961
- return lines.join("\n");
1962
- }
1963
- function mergeMission(fallback, parsed) {
1964
- if (!parsed) return fallback;
1965
- const mode = parsed.mode === "strict_target_reply" || parsed.mode === "strict_target_interaction" || parsed.mode === "persona_balanced" ? parsed.mode : fallback.mode;
1966
- const objective = typeof parsed.objective === "string" && parsed.objective.trim().length > 0 ? parsed.objective.trim() : fallback.objective;
1967
- const rationale = typeof parsed.rationale === "string" && parsed.rationale.trim().length > 0 ? parsed.rationale.trim() : fallback.rationale;
1968
- const focusTopics = Array.isArray(parsed.focusTopics) ? unique2(
1969
- parsed.focusTopics.map((topic) => String(topic).trim()).filter(Boolean).slice(0, 10)
1970
- ) : fallback.focusTopics;
1971
- const targetHandles = Array.isArray(parsed.targetHandles) ? unique2(
1972
- parsed.targetHandles.map((handle) => normalizeHandle4(String(handle))).filter(Boolean)
1973
- ) : fallback.targetHandles;
1974
- let allowedActions = sanitizeActions(parsed.allowedActions, fallback.allowedActions);
1975
- const mustReplyOnly = mode === "strict_target_reply";
1976
- if (mustReplyOnly) {
1977
- allowedActions = ["reply"];
1978
- }
1979
- const hasStrictTargets = fallback.targetHandles.length > 0;
1980
- if (hasStrictTargets) {
1981
- return {
1982
- ...fallback,
1983
- mode: mustReplyOnly ? "strict_target_reply" : mode,
1984
- objective,
1985
- rationale,
1986
- focusTopics,
1987
- allowedActions,
1988
- targetHandles: fallback.targetHandles
1989
- };
1990
- }
1991
- return {
1992
- mode,
1993
- objective,
1994
- rationale,
1995
- focusTopics,
1996
- allowedActions,
1997
- targetHandles
1998
- };
1999
- }
2000
- async function createHeartbeatMission(research) {
2001
- const identity = loadIdentity();
2002
- const strategy = loadStrategy();
2003
- const constraints = getPersonaConstraints(identity);
2004
- const profile = compilePersonaActionProfile({ identity, strategy, constraints });
2005
- const fallback = buildDeterministicMission(research, profile);
2006
- if (fallback.mode !== "persona_balanced") {
2007
- return fallback;
2008
- }
2009
- const system = [
2010
- "You are a mission director for an autonomous X agent.",
2011
- "Your output must be strict JSON only.",
2012
- "Prioritize persona fidelity over engagement.",
2013
- "Never violate explicit persona constraints."
2014
- ].join(" ");
2015
- try {
2016
- const prompt = buildMissionPrompt(research, fallback, profile);
2017
- const response = await generateResponse(system, prompt);
2018
- const parsed = parseJsonObject(response.content);
2019
- return mergeMission(fallback, parsed);
2020
- } catch {
2021
- return fallback;
2022
- }
2023
- }
2024
-
2025
- // src/runtime/bandit.ts
2026
- import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
2027
- var DEFAULT_EXPLORATION_BUDGET = 0.25;
2028
- function defaultState() {
2029
- return {
2030
- totalPulls: 0,
2031
- explorationBudget: DEFAULT_EXPLORATION_BUDGET,
2032
- arms: {},
2033
- pending: []
2034
- };
2035
- }
2036
- function clamp(value, min, max) {
2037
- return Math.max(min, Math.min(max, value));
2038
- }
2039
- function loadState() {
2040
- if (!existsSync2(paths.bandit)) return defaultState();
2041
- try {
2042
- const parsed = JSON.parse(readFileSync2(paths.bandit, "utf-8"));
2043
- const state = {
2044
- totalPulls: Math.max(0, parsed.totalPulls ?? 0),
2045
- explorationBudget: clamp(parsed.explorationBudget ?? DEFAULT_EXPLORATION_BUDGET, 0.1, 0.45),
2046
- arms: parsed.arms ?? {},
2047
- pending: Array.isArray(parsed.pending) ? parsed.pending : []
2048
- };
2049
- return state;
2050
- } catch {
2051
- return defaultState();
2052
- }
2053
- }
2054
- function saveState(state) {
2055
- state.explorationBudget = clamp(state.explorationBudget, 0.1, 0.45);
2056
- state.pending = state.pending.filter((entry) => {
2057
- if (!entry.resolved) return true;
2058
- const resolvedAtMs = Date.parse(entry.resolveAt);
2059
- if (Number.isNaN(resolvedAtMs)) return false;
2060
- return Date.now() - resolvedAtMs < 7 * 24 * 60 * 60 * 1e3;
2061
- }).slice(-1200);
2062
- writeFileSync2(paths.bandit, JSON.stringify(state, null, 2));
2063
- }
2064
- function ensureArm(state, armKey2) {
2065
- const existing = state.arms[armKey2];
2066
- if (existing) return existing;
2067
- const arm = {
2068
- key: armKey2,
2069
- pulls: 0,
2070
- rewardSum: 0,
2071
- lastReward: 0,
2072
- updatedAt: (/* @__PURE__ */ new Date()).toISOString()
2073
- };
2074
- state.arms[armKey2] = arm;
2075
- return arm;
2076
- }
2077
- function averageReward(arm) {
2078
- if (arm.pulls <= 0) return 0;
2079
- return arm.rewardSum / arm.pulls;
2080
- }
2081
- function ucbBonus(totalPulls, armPulls) {
2082
- const t = Math.max(1, totalPulls);
2083
- return Math.sqrt(2 * Math.log(t + 1) / (armPulls + 1));
2084
- }
2085
- function computeImmediateReward(result) {
2086
- if (result.success) return 0.12;
2087
- const error = (result.error ?? "").toLowerCase();
2088
- if (error.includes("duplicate content")) return -0.95;
2089
- if (error.includes("rate limit")) return -0.5;
2090
- if (error.includes("forbidden") || error.includes("403")) return -0.55;
2091
- return -0.35;
2092
- }
2093
- function resolveDelayedReward(metrics) {
2094
- if (!metrics) return -0.05;
2095
- const reward = Math.min(metrics.likeCount * 0.03, 0.9) + Math.min(metrics.replyCount * 0.12, 0.9) + Math.min(metrics.retweetCount * 0.08, 0.8);
2096
- if (metrics.likeCount === 0 && metrics.replyCount === 0 && metrics.retweetCount === 0) {
2097
- return -0.15;
2098
- }
2099
- return Math.min(reward, 1.8);
2100
- }
2101
- function armKeyFromAction(action) {
2102
- if (action.banditArm && action.banditArm.trim().length > 0) return action.banditArm;
2103
- const source = action.source ?? "unknown";
2104
- return `${action.action}:${source}`;
2105
- }
2106
- function extractTweetId(action, result) {
2107
- if (result.detail && /^\d+$/.test(result.detail)) return result.detail;
2108
- if (action.action !== "reply" && action.tweetId) return action.tweetId;
2109
- return void 0;
2110
- }
2111
- function selectBanditOpportunityPool(opportunities, maxPoolSize = 24) {
2112
- if (opportunities.length <= maxPoolSize) return opportunities;
2113
- const state = loadState();
2114
- const totalPulls = Math.max(1, state.totalPulls);
2115
- const scored = opportunities.map((opportunity) => {
2116
- const arm = ensureArm(state, opportunity.armKey);
2117
- const mean = averageReward(arm);
2118
- const bonus = ucbBonus(totalPulls, arm.pulls);
2119
- const coldStartBoost = arm.pulls === 0 ? 0.25 : 0;
2120
- const adjustedScore = opportunity.score + mean * 1.5 + bonus * 0.35 + coldStartBoost;
2121
- return {
2122
- opportunity: {
2123
- ...opportunity,
2124
- score: adjustedScore
2125
- },
2126
- adjustedScore,
2127
- pulls: arm.pulls
2128
- };
2129
- });
2130
- const explorationBudget = clamp(state.explorationBudget, 0.1, 0.45);
2131
- const exploreCount = Math.max(1, Math.floor(maxPoolSize * explorationBudget));
2132
- const exploitCount = Math.max(1, maxPoolSize - exploreCount);
2133
- const exploit = [...scored].sort((a, b) => b.adjustedScore - a.adjustedScore).slice(0, exploitCount);
2134
- const selected = /* @__PURE__ */ new Map();
2135
- for (const row of exploit) {
2136
- selected.set(row.opportunity.id, row.opportunity);
2137
- }
2138
- const remaining = scored.filter((row) => !selected.has(row.opportunity.id)).sort((a, b) => {
2139
- if (a.pulls !== b.pulls) return a.pulls - b.pulls;
2140
- return b.adjustedScore - a.adjustedScore;
2141
- });
2142
- while (selected.size < maxPoolSize && remaining.length > 0) {
2143
- const window = remaining.splice(0, Math.min(5, remaining.length));
2144
- const pick = window[Math.floor(Math.random() * window.length)];
2145
- if (!selected.has(pick.opportunity.id)) {
2146
- selected.set(pick.opportunity.id, pick.opportunity);
2147
- }
2148
- for (const row of window) {
2149
- if (row.opportunity.id !== pick.opportunity.id) {
2150
- remaining.push(row);
2151
- }
2152
- }
2153
- remaining.sort((a, b) => {
2154
- if (a.pulls !== b.pulls) return a.pulls - b.pulls;
2155
- return b.adjustedScore - a.adjustedScore;
2156
- });
2157
- }
2158
- const picked = [...selected.values()].sort((a, b) => b.score - a.score).slice(0, maxPoolSize);
2159
- saveState(state);
2160
- return picked;
2161
- }
2162
- function recordBanditActionResults(actions, results) {
2163
- if (actions.length === 0 || results.length === 0) return;
2164
- const state = loadState();
2165
- const now = (/* @__PURE__ */ new Date()).toISOString();
2166
- for (let i = 0; i < Math.min(actions.length, results.length); i += 1) {
2167
- const action = actions[i];
2168
- const result = results[i];
2169
- const armKey2 = armKeyFromAction(action);
2170
- const arm = ensureArm(state, armKey2);
2171
- const immediateReward = computeImmediateReward(result);
2172
- arm.pulls += 1;
2173
- arm.rewardSum += immediateReward;
2174
- arm.lastReward = immediateReward;
2175
- arm.updatedAt = now;
2176
- state.totalPulls += 1;
2177
- const tweetId = extractTweetId(action, result);
2178
- if (result.success && tweetId && (action.action === "post" || action.action === "reply")) {
2179
- const delayMs = action.action === "reply" ? 20 * 60 * 1e3 : 45 * 60 * 1e3;
2180
- state.pending.push({
2181
- id: `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
2182
- armKey: armKey2,
2183
- actionType: action.action,
2184
- tweetId,
2185
- createdAt: now,
2186
- resolveAt: new Date(Date.now() + delayMs).toISOString(),
2187
- resolved: false
2188
- });
2189
- }
2190
- }
2191
- saveState(state);
2192
- }
2193
- async function collectDelayedOutcomes(client) {
2194
- const state = loadState();
2195
- const now = Date.now();
2196
- const pendingDue = state.pending.filter((entry) => !entry.resolved && Date.parse(entry.resolveAt) <= now).slice(0, 8);
2197
- if (pendingDue.length === 0) {
2198
- return { processed: 0, resolved: 0, rewardAdded: 0 };
2199
- }
2200
- let rewardAdded = 0;
2201
- let resolved = 0;
2202
- const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
2203
- for (const pending of pendingDue) {
2204
- const arm = ensureArm(state, pending.armKey);
2205
- let metrics = null;
2206
- if (pending.tweetId) {
2207
- metrics = await client.getTweetMetrics(pending.tweetId);
2208
- if (metrics) {
2209
- updatePostMetrics(pending.tweetId, {
2210
- checkedAt,
2211
- likes: metrics.likeCount,
2212
- retweets: metrics.retweetCount,
2213
- replies: metrics.replyCount
2214
- });
2215
- }
2216
- }
2217
- const reward = resolveDelayedReward(
2218
- metrics ? {
2219
- likeCount: metrics.likeCount,
2220
- retweetCount: metrics.retweetCount,
2221
- replyCount: metrics.replyCount
2222
- } : null
2223
- );
2224
- arm.rewardSum += reward;
2225
- arm.lastReward = reward;
2226
- arm.updatedAt = checkedAt;
2227
- rewardAdded += reward;
2228
- pending.resolved = true;
2229
- resolved += 1;
2230
- }
2231
- saveState(state);
2232
- logger.info(
2233
- `Bandit delayed outcomes processed=${pendingDue.length}, resolved=${resolved}, rewardDelta=${rewardAdded.toFixed(2)}`
2234
- );
2235
- return {
2236
- processed: pendingDue.length,
2237
- resolved,
2238
- rewardAdded
2239
- };
2240
- }
2241
-
2242
- // src/runtime/autonomy.ts
2243
- function recentReplyTargets(hours = 24) {
2244
- const cutoffMs = Date.now() - hours * 60 * 60 * 1e3;
2245
- const recent = getRecentInteractions(300);
2246
- const targets = /* @__PURE__ */ new Set();
2247
- for (const entry of recent) {
2248
- if (entry.type !== "reply") continue;
2249
- if (!entry.inReplyTo) continue;
2250
- const ts = Date.parse(entry.timestamp);
2251
- if (!Number.isNaN(ts) && ts < cutoffMs) continue;
2252
- targets.add(entry.inReplyTo);
2253
- }
2254
- return targets;
2255
- }
2256
- function selfUserIdFromCredentials() {
2257
- try {
2258
- const creds = loadCredentials();
2259
- const accessToken = creds.accessToken;
2260
- if (!accessToken) return null;
2261
- const dashIdx = accessToken.indexOf("-");
2262
- if (dashIdx <= 0) return null;
2263
- const candidate = accessToken.substring(0, dashIdx);
2264
- return /^\d+$/.test(candidate) ? candidate : null;
2265
- } catch {
2266
- return null;
2267
- }
2268
- }
2269
- function normalizeHandle5(handle) {
2270
- return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
2271
- }
2272
- function roughWordCount(text) {
2273
- return text.trim().split(/\s+/).filter(Boolean).length;
2274
- }
2275
- function inferStyleMode(text) {
2276
- const lower = text.toLowerCase();
2277
- if (text.includes("?")) return "curious_question";
2278
- if (/\b(lol|lmao|haha|wild|fr|ngl)\b/i.test(text)) return "playful_line";
2279
- if (/\b(not|isn'?t|wrong|nah|nope|doesn'?t)\b/i.test(lower)) return "friendly_pushback";
2280
- if (roughWordCount(text) <= 9) return "quick_reaction";
2281
- return "plain_observation";
2282
- }
2283
- function chooseRewriteStyleMode(recentTexts, targetTweetText) {
2284
- const counts = {
2285
- quick_reaction: 0,
2286
- curious_question: 0,
2287
- friendly_pushback: 0,
2288
- plain_observation: 0,
2289
- playful_line: 0
2290
- };
2291
- for (const text of (recentTexts ?? []).slice(0, 10)) {
2292
- counts[inferStyleMode(text)] += 1;
2293
- }
2294
- if ((targetTweetText ?? "").includes("?") && counts.curious_question <= 2) {
2295
- return "curious_question";
2296
- }
2297
- return Object.entries(counts).sort((a, b) => a[1] - b[1])[0]?.[0] ?? "plain_observation";
2298
- }
2299
- function rewriteStyleModeInstructions(mode) {
2300
- if (mode === "quick_reaction") {
2301
- return [
2302
- "- style mode: quick reaction",
2303
- "- 6-14 words, one short sentence",
2304
- "- immediate reaction tone, no thesis statement"
2305
- ];
2306
- }
2307
- if (mode === "curious_question") {
2308
- return [
2309
- "- style mode: curious question",
2310
- "- include exactly one direct question",
2311
- "- ask about a specific detail from the target tweet"
2312
- ];
2313
- }
2314
- if (mode === "friendly_pushback") {
2315
- return [
2316
- "- style mode: friendly pushback",
2317
- "- politely disagree in plain words",
2318
- "- no lecture or abstract framing"
2319
- ];
2320
- }
2321
- if (mode === "playful_line") {
2322
- return [
2323
- "- style mode: playful line",
2324
- "- keep it light and witty, not sarcastic essay mode",
2325
- "- avoid internet clich\xE9s and overused slogans"
2326
- ];
2327
- }
2328
- return [
2329
- "- style mode: plain observation",
2330
- "- write like a natural human thought said out loud",
2331
- "- concrete detail first, no broad generalization"
2332
- ];
2333
- }
2334
- function shouldAttemptStyleRewrite(action, reason) {
2335
- if (!action.content || action.action !== "reply" && action.action !== "post") return false;
2336
- const lower = reason.toLowerCase();
2337
- return lower.includes("lecture-like") || lower.includes("abstract philosopher cadence") || lower.includes("manifesto") || lower.includes("too long/explanatory") || lower.includes("grounded in the target tweet context") || lower.includes("hedged consensus phrasing") || lower.includes("repetitive anchor phrase/term") || lower.includes("repetitive opening phrase") || lower.includes("low-novelty vocabulary loop") || lower.includes("abstract wording without concrete anchor");
2338
- }
2339
- function personaAdjustedOpportunityScore(opportunity, profile) {
2340
- const actionType = opportunity.actionType;
2341
- const source = opportunity.source;
2342
- const actionBias = profile.actionBias[actionType] ?? 0;
2343
- const sourceBias = profile.sourceBias[source] ?? 0;
2344
- return opportunity.score + actionBias * 0.9 + sourceBias * 0.55;
2345
- }
2346
- function buildPersonaGeneratedPool(opportunities, maxSize, profile) {
2347
- if (opportunities.length <= 1) return opportunities.slice(0, maxSize);
2348
- const maxPool = Math.max(1, Math.min(maxSize, opportunities.length));
2349
- const byAction = /* @__PURE__ */ new Map();
2350
- for (const action of ["reply", "like", "retweet", "follow", "post"]) {
2351
- byAction.set(action, []);
2352
- }
2353
- for (const opportunity of opportunities) {
2354
- const action = opportunity.actionType;
2355
- byAction.get(action)?.push(opportunity);
2356
- }
2357
- for (const [action, rows] of byAction.entries()) {
2358
- rows.sort((a, b) => personaAdjustedOpportunityScore(b, profile) - personaAdjustedOpportunityScore(a, profile));
2359
- byAction.set(action, rows);
2360
- }
2361
- const actionOrder = profile.prioritizedActions.filter((action) => (byAction.get(action)?.length ?? 0) > 0);
2362
- const weightedActions = actionOrder.map((action) => ({
2363
- action,
2364
- weight: Math.max(0.2, profile.actionBias[action] + 1.25)
2365
- })).filter((row) => row.weight > 0);
2366
- const selected = /* @__PURE__ */ new Map();
2367
- if (weightedActions.length > 0) {
2368
- const totalWeight = weightedActions.reduce((sum, row) => sum + row.weight, 0);
2369
- const quotas = /* @__PURE__ */ new Map();
2370
- for (const row of weightedActions) {
2371
- quotas.set(row.action, Math.max(0, Math.floor(row.weight / totalWeight * maxPool)));
2372
- }
2373
- for (const topAction of actionOrder.slice(0, 2)) {
2374
- const rows = byAction.get(topAction) ?? [];
2375
- if (rows.length === 0) continue;
2376
- quotas.set(topAction, Math.max(1, quotas.get(topAction) ?? 0));
2377
- }
2378
- for (const row of weightedActions) {
2379
- const rows = byAction.get(row.action) ?? [];
2380
- const limit = quotas.get(row.action) ?? 0;
2381
- for (const candidate of rows.slice(0, limit)) {
2382
- if (selected.size >= maxPool) break;
2383
- selected.set(candidate.id, candidate);
2384
- }
2385
- if (selected.size >= maxPool) break;
2386
- }
2387
- }
2388
- const globalSorted = [...opportunities].sort(
2389
- (a, b) => personaAdjustedOpportunityScore(b, profile) - personaAdjustedOpportunityScore(a, profile)
2390
- );
2391
- for (const candidate of globalSorted) {
2392
- if (selected.size >= maxPool) break;
2393
- selected.set(candidate.id, candidate);
2394
- }
2395
- return [...selected.values()].slice(0, maxPool);
2396
- }
2397
- function cleanRewriteOutput(text) {
2398
- const cleaned = text.replace(/```[\s\S]*?```/g, "").replace(/^["'`]+|["'`]+$/g, "").trim();
2399
- const firstLine = cleaned.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? cleaned;
2400
- return firstLine.trim();
2401
- }
2402
- async function rewriteDraftForHumanVoice(input) {
2403
- if (!input.action.content) return null;
2404
- const identity = loadIdentity();
2405
- const maxChars = input.action.action === "reply" ? 100 : 130;
2406
- const minChars = input.action.action === "reply" ? 18 : 24;
2407
- const styleMode = chooseRewriteStyleMode(input.recentTexts, input.targetTweetText);
2408
- const system = [
2409
- `You rewrite X/Twitter drafts into ${identity.name}'s natural voice.`,
2410
- "Output must feel human, direct, and contextual.",
2411
- "Never write abstract philosophy or explanatory manifesto style.",
2412
- "Return ONLY rewritten tweet text, no commentary."
2413
- ].join(" ");
2414
- const promptParts = [];
2415
- promptParts.push(`Current draft: ${input.action.content}`);
2416
- promptParts.push(`Rejected because: ${input.reason}`);
2417
- if (input.targetTweetText) {
2418
- promptParts.push(`Target tweet context: ${input.targetTweetText}`);
2419
- }
2420
- promptParts.push(`Constraints:`);
2421
- promptParts.push(`- ${minChars}-${maxChars} characters`);
2422
- promptParts.push("- 1-2 short sentences max");
2423
- promptParts.push("- no 'the real question' / 'the deeper question' framing");
2424
- promptParts.push("- no colon, semicolon, or em dash");
2425
- promptParts.push("- specific and concrete, not abstract");
2426
- promptParts.push("- keep the same core stance");
2427
- promptParts.push("- use natural spoken phrasing and contractions when it fits");
2428
- for (const line of rewriteStyleModeInstructions(styleMode)) {
2429
- promptParts.push(line);
2430
- }
2431
- if (input.recentTexts && input.recentTexts.length > 0) {
2432
- promptParts.push("- avoid vocabulary anchors from these recent outputs:");
2433
- for (const text of input.recentTexts.slice(0, 5)) {
2434
- promptParts.push(` - ${text.slice(0, 120)}`);
2435
- }
2436
- }
2437
- try {
2438
- const rewrite = await generateResponse(system, promptParts.join("\n"));
2439
- const candidate = cleanRewriteOutput(rewrite.content);
2440
- if (!candidate) return null;
2441
- return candidate;
2442
- } catch {
2443
- return null;
2444
- }
2445
- }
2446
- async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2447
- const client = await getXClient();
2448
- const identity = loadIdentity();
2449
- const strategy = loadStrategy();
2450
- const constraints = getPersonaConstraints();
2451
- const personaProfile = compilePersonaActionProfile({ identity, strategy, constraints });
2452
- const strictReplyOnly = constraints.replyOnlyMode || constraints.onlyReplyToHandles.length > 0;
2453
- const interactionActions = /* @__PURE__ */ new Set(["reply", "like", "retweet", "follow"]);
2454
- const constraintLines = buildPersonaConstraintLines(constraints);
2455
- if (constraintLines.length > 0) {
2456
- logger.info(`Persona constraints active: ${constraintLines.join(" | ")}`);
2457
- }
2458
- logger.info(
2459
- `Persona action profile: actions=${personaProfile.prioritizedActions.slice(0, 4).join(" > ")}, sources=${Object.entries(personaProfile.sourceBias).sort((a, b) => b[1] - a[1]).map(([source]) => source).slice(0, 3).join(" > ")}`
2460
- );
2461
- try {
2462
- const delayed = await collectDelayedOutcomes(client);
2463
- if (delayed.processed > 0) {
2464
- logger.info(
2465
- `Delayed outcomes: processed=${delayed.processed}, resolved=${delayed.resolved}, rewardDelta=${delayed.rewardAdded.toFixed(2)}`
2466
- );
2467
- }
2468
- } catch (error) {
2469
- logger.warn(`Delayed outcome collection failed: ${error.message}`);
2470
- }
2471
- const selfHandle = identity.handle.replace(/^@/, "").toLowerCase();
2472
- const selfUserId = selfUserIdFromCredentials();
2473
- const isSelfTweet = (tweet) => tweet.authorHandle.replace(/^@/, "").toLowerCase() === selfHandle || selfUserId !== null && tweet.authorId === selfUserId;
2474
- const research = await runResearchPhase(client, heartbeatCount);
2475
- const timeline = research.timeline.filter((tweet) => !isSelfTweet(tweet));
2476
- const mentions = research.mentions.filter((tweet) => !isSelfTweet(tweet));
2477
- const recentReplyTargetIds = recentReplyTargets(24);
2478
- const filteredTimeline = timeline.filter((tweet) => !recentReplyTargetIds.has(tweet.id));
2479
- const filteredMentions = mentions.filter((tweet) => !recentReplyTargetIds.has(tweet.id));
2480
- const filteredTopicSearchResults = research.topicSearchResults.map((result) => ({
2481
- ...result,
2482
- tweets: result.tweets.filter((tweet) => !recentReplyTargetIds.has(tweet.id) && !isSelfTweet(tweet))
2483
- })).filter((result) => result.tweets.length > 0);
2484
- const filteredPeopleActivity = research.peopleActivity.map((person) => ({
2485
- ...person,
2486
- tweets: person.tweets.filter((tweet) => !recentReplyTargetIds.has(tweet.id) && !isSelfTweet(tweet))
2487
- })).filter((person) => person.tweets.length > 0);
2488
- const observedTweetIds = [
2489
- ...filteredTimeline.map((tweet) => tweet.id),
2490
- ...filteredMentions.map((tweet) => tweet.id),
2491
- ...filteredTopicSearchResults.flatMap((result) => result.tweets.map((tweet) => tweet.id)),
2492
- ...filteredPeopleActivity.flatMap((person) => person.tweets.map((tweet) => tweet.id))
2493
- ];
2494
- const observedTweets = [
2495
- ...filteredTimeline,
2496
- ...filteredMentions,
2497
- ...filteredTopicSearchResults.flatMap((result) => result.tweets),
2498
- ...filteredPeopleActivity.flatMap((person) => person.tweets)
2499
- ];
2500
- const mission = await createHeartbeatMission({
2501
- ...research,
2502
- timeline: filteredTimeline,
2503
- mentions: filteredMentions,
2504
- topicSearchResults: filteredTopicSearchResults,
2505
- peopleActivity: filteredPeopleActivity
2506
- });
2507
- logger.info(
2508
- `Mission: mode=${mission.mode}, objective="${mission.objective}", allowed=${mission.allowedActions.join(",")}, targets=${mission.targetHandles.length > 0 ? mission.targetHandles.map((h) => `@${h}`).join(",") : "none"}`
2509
- );
2510
- logger.info(
2511
- `Autonomy context: timeline=${filteredTimeline.length}, mentions=${filteredMentions.length}, topicTweets=${filteredTopicSearchResults.reduce((sum, r) => sum + r.tweets.length, 0)}, peopleTweets=${filteredPeopleActivity.reduce((sum, p) => sum + p.tweets.length, 0)}`
2512
- );
2513
- const systemPrompt = buildSystemPrompt();
2514
- const actions = [];
2515
- const results = [];
2516
- const policyFeedback = [
2517
- `Heartbeat mission: ${mission.objective}`,
2518
- `Mission mode: ${mission.mode}`
2519
- ];
2520
- const blockedTweetIds = new Set(recentReplyTargetIds);
2521
- const allowedActionSet = new Set(mission.allowedActions);
2522
- const disallowedActions = new Set(
2523
- ["reply", "like", "retweet", "follow", "post"].filter((action) => !allowedActionSet.has(action))
2524
- );
2525
- if (mission.targetHandles.length > 0) {
2526
- policyFeedback.push(`Mission targets: ${mission.targetHandles.map((handle) => `@${handle}`).join(", ")}`);
2527
- }
2528
- if (mission.focusTopics.length > 0) {
2529
- policyFeedback.push(`Mission focus: ${mission.focusTopics.slice(0, 5).join(", ")}`);
2530
- }
2531
- policyFeedback.push(`Mission rationale: ${mission.rationale}`);
2532
- const missionTargetSet = new Set(mission.targetHandles.map((handle) => normalizeHandle5(handle)).filter(Boolean));
2533
- let replyRejectionCount = 0;
2534
- const opportunities = buildActionOpportunities({
2535
- research: {
2536
- ...research,
2537
- timeline: filteredTimeline,
2538
- mentions: filteredMentions,
2539
- topicSearchResults: filteredTopicSearchResults,
2540
- peopleActivity: filteredPeopleActivity
2541
- },
2542
- selfHandle,
2543
- selfUserId,
2544
- maxCandidates: 30
2545
- });
2546
- const personaSeedPool = buildPersonaGeneratedPool(opportunities, 30, personaProfile);
2547
- const banditPool = selectBanditOpportunityPool(personaSeedPool, 24);
2548
- const planningPool = buildPersonaGeneratedPool(banditPool, 24, personaProfile);
2549
- const byType = planningPool.reduce((acc, opportunity) => {
2550
- acc[opportunity.actionType] = (acc[opportunity.actionType] ?? 0) + 1;
2551
- return acc;
2552
- }, {});
2553
- logger.info(`Opportunities selected: total=${planningPool.length}, byType=${JSON.stringify(byType)}`);
2554
- const activeIntents = listIntents();
2555
- const intentTargetHandles = new Set(
2556
- activeIntents.flatMap((intent) => intent.targetHandles.map((handle) => normalizeHandle5(handle)))
2557
- );
2558
- const priorityHandles = /* @__PURE__ */ new Set(
2559
- [
2560
- ...strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => normalizeHandle5(person.handle)).filter(Boolean),
2561
- ...intentTargetHandles
2562
- ]
2563
- );
2564
- const authorByTweetId = /* @__PURE__ */ new Map();
2565
- for (const opportunity of planningPool) {
2566
- if (opportunity.tweetId && opportunity.authorHandle) {
2567
- authorByTweetId.set(opportunity.tweetId, normalizeHandle5(opportunity.authorHandle));
2568
- }
2569
- }
2570
- getRecentInteractions(20);
2571
- const getPlanningOpportunities = () => {
2572
- return planningPool.filter((opportunity) => {
2573
- if (disallowedActions.has(opportunity.actionType)) return false;
2574
- if (opportunity.tweetId && blockedTweetIds.has(opportunity.tweetId)) return false;
2575
- if (missionTargetSet.size > 0 && interactionActions.has(opportunity.actionType)) {
2576
- const author = normalizeHandle5(opportunity.authorHandle);
2577
- if (!author || !missionTargetSet.has(author)) return false;
2578
- }
2579
- return true;
2580
- });
2581
- };
2582
- for (let planningRound = 0; planningRound < 2 && actions.length < maxActions; planningRound += 1) {
2583
- const candidates = getPlanningOpportunities();
2584
- if (candidates.length === 0) {
2585
- logger.info("No viable opportunities after filtering.");
2586
- break;
2587
- }
2588
- let plannedActions = await planActionPortfolio({
2589
- systemPrompt,
2590
- opportunities: candidates,
2591
- maxActions: maxActions - actions.length,
2592
- policyFeedback,
2593
- executedActions: actions
2594
- });
2595
- if (!strictReplyOnly && plannedActions.length > 1 && plannedActions.every((a) => a.action === "reply")) {
2596
- const nonReplyCandidate = candidates.find(
2597
- (opportunity) => opportunity.actionType !== "reply" && !opportunity.requiresContent
2598
- );
2599
- if (nonReplyCandidate) {
2600
- plannedActions = [
2601
- { ...nonReplyCandidate.template, reasoning: "diversity pivot" },
2602
- ...plannedActions.slice(0, Math.max(0, maxActions - actions.length - 1))
2603
- ];
2604
- }
2605
- }
2606
- if (priorityHandles.size > 0) {
2607
- const authorByTweetId2 = /* @__PURE__ */ new Map();
2608
- for (const opportunity of candidates) {
2609
- if (opportunity.tweetId && opportunity.authorHandle) {
2610
- authorByTweetId2.set(opportunity.tweetId, normalizeHandle5(opportunity.authorHandle));
2611
- }
2612
- }
2613
- const touchesPriorityTarget = plannedActions.some((action) => {
2614
- if (action.handle && priorityHandles.has(normalizeHandle5(action.handle))) return true;
2615
- if (action.targetHandle && priorityHandles.has(normalizeHandle5(action.targetHandle))) return true;
2616
- if (action.tweetId && priorityHandles.has(authorByTweetId2.get(action.tweetId) ?? "")) return true;
2617
- return false;
2618
- });
2619
- if (!touchesPriorityTarget) {
2620
- const priorityCandidate = candidates.find(
2621
- (opportunity) => Boolean(opportunity.authorHandle) && priorityHandles.has(normalizeHandle5(opportunity.authorHandle)) && (!strictReplyOnly ? !opportunity.requiresContent : opportunity.actionType === "reply")
2622
- );
2623
- if (priorityCandidate) {
2624
- plannedActions = [
2625
- { ...priorityCandidate.template, reasoning: "priority-target focus" },
2626
- ...plannedActions
2627
- ].slice(0, Math.max(0, maxActions - actions.length));
2628
- logger.info(`Priority target injection: @${priorityCandidate.authorHandle}`);
2629
- }
2630
- }
2631
- }
2632
- if (plannedActions.length === 0) {
2633
- logger.info("Portfolio planner returned no actionable selections.");
2634
- break;
2635
- }
2636
- let acceptedInRound = 0;
2637
- for (const proposedAction of plannedActions) {
2638
- let candidateAction = proposedAction;
2639
- if (actions.length >= maxActions) break;
2640
- if (disallowedActions.has(candidateAction.action)) {
2641
- const reason = `Action ${candidateAction.action} is temporarily disallowed this heartbeat.`;
2642
- policyFeedback.push(reason);
2643
- logger.info(`Policy rejected action ${candidateAction.action}: ${reason}`);
2644
- continue;
2645
- }
2646
- if (candidateAction.tweetId && blockedTweetIds.has(candidateAction.tweetId)) {
2647
- const reason = `Tweet ${candidateAction.tweetId} is blocked for this heartbeat.`;
2648
- policyFeedback.push(reason);
2649
- logger.info(`Policy rejected action ${candidateAction.action}: ${reason}`);
2650
- continue;
2651
- }
2652
- let policy = evaluateActionPolicy({
2653
- action: candidateAction,
2654
- step: actions.length,
2655
- timeline: filteredTimeline,
2656
- mentions: filteredMentions,
2657
- executedActions: actions,
2658
- observedTweetIds,
2659
- observedTweets,
2660
- selfHandle,
2661
- selfUserId
2662
- });
2663
- if (!policy.allowed && shouldAttemptStyleRewrite(candidateAction, policy.reason ?? "")) {
2664
- const recentTexts = getRecentInteractions(10).filter((entry) => entry.type === "post" || entry.type === "reply").map((entry) => entry.content ?? "").filter(Boolean);
2665
- const rewritten = await rewriteDraftForHumanVoice({
2666
- action: candidateAction,
2667
- reason: policy.reason ?? "policy rejected",
2668
- targetTweetText: candidateAction.tweetId ? observedTweets.find((t) => t.id === candidateAction.tweetId)?.text : void 0,
2669
- recentTexts
2670
- });
2671
- if (rewritten && rewritten !== candidateAction.content) {
2672
- candidateAction = {
2673
- ...candidateAction,
2674
- content: rewritten,
2675
- reasoning: `${candidateAction.reasoning ?? "rewritten"} | style rewrite`
2676
- };
2677
- policy = evaluateActionPolicy({
2678
- action: candidateAction,
2679
- step: actions.length,
2680
- timeline: filteredTimeline,
2681
- mentions: filteredMentions,
2682
- executedActions: actions,
2683
- observedTweetIds,
2684
- observedTweets,
2685
- selfHandle,
2686
- selfUserId
2687
- });
2688
- }
2689
- }
2690
- if (!policy.allowed) {
2691
- const reason = policy.reason ?? "Policy rejected action";
2692
- policyFeedback.push(reason);
2693
- logger.info(`Policy rejected action ${candidateAction.action}: ${reason}`);
2694
- if (candidateAction.tweetId) {
2695
- blockedTweetIds.add(candidateAction.tweetId);
2696
- }
2697
- if (candidateAction.action === "reply") {
2698
- replyRejectionCount += 1;
2699
- if (replyRejectionCount >= 2 && !strictReplyOnly) {
2700
- disallowedActions.add("reply");
2701
- const pivot = "Reply opportunities exhausted this heartbeat. Pivot to like/retweet/follow/post.";
2702
- policyFeedback.push(pivot);
2703
- logger.info(`Policy adjustment: ${pivot}`);
2704
- }
2705
- }
2706
- continue;
2707
- }
2708
- acceptedInRound += 1;
2709
- const result = await executeAction(candidateAction);
2710
- actions.push(candidateAction);
2711
- results.push(result);
2712
- if (activeIntents.length > 0) {
2713
- const touchedHandle = normalizeHandle5(
2714
- candidateAction.targetHandle ?? candidateAction.handle ?? (candidateAction.tweetId ? authorByTweetId.get(candidateAction.tweetId) : void 0)
2715
- );
2716
- const loweredContent = (candidateAction.content ?? "").toLowerCase();
2717
- for (const intent of activeIntents) {
2718
- const handleMatch = touchedHandle.length > 0 && intent.targetHandles.some((handle) => normalizeHandle5(handle) === touchedHandle);
2719
- const topicMatch = loweredContent.length > 0 && intent.focusTopics.some((topic) => loweredContent.includes(topic.toLowerCase()));
2720
- if (!handleMatch && !topicMatch) continue;
2721
- recordIntentExecution(intent.id, {
2722
- success: result.success,
2723
- note: `${candidateAction.action}${touchedHandle ? ` @${touchedHandle}` : ""}`
2724
- });
2725
- }
2726
- }
2727
- if (!result.success) {
2728
- const err = result.error ?? "";
2729
- if (candidateAction.tweetId) {
2730
- blockedTweetIds.add(candidateAction.tweetId);
2731
- }
2732
- if (candidateAction.action === "reply" && /duplicate content/i.test(err)) {
2733
- if (!strictReplyOnly) {
2734
- disallowedActions.add("reply");
2735
- const reason = "Reply failed with duplicate-content error. Switch to non-reply actions.";
2736
- policyFeedback.push(reason);
2737
- logger.info(`Policy adjustment: ${reason}`);
2738
- } else {
2739
- const reason = "Reply failed with duplicate-content error. Keep reply mode but use a new angle/target.";
2740
- policyFeedback.push(reason);
2741
- logger.info(`Policy adjustment: ${reason}`);
2742
- }
2743
- }
2744
- if ((candidateAction.action === "post" || candidateAction.action === "schedule") && /duplicate content/i.test(err)) {
2745
- const reason = "Write-path duplicate-content failure. Change framing and try a different angle.";
2746
- policyFeedback.push(reason);
2747
- logger.info(`Policy adjustment: ${reason}`);
2748
- }
2749
- }
2750
- }
2751
- if (acceptedInRound === 0) {
2752
- logger.info("Planner round produced no policy-approved actions.");
2753
- continue;
2754
- }
2755
- }
2756
- recordBanditActionResults(actions, results);
2757
- return {
2758
- timeline,
2759
- mentions,
2760
- actions,
2761
- results,
2762
- policyFeedback
2763
- };
2764
- }
2765
-
2766
- export {
2767
- runAutonomyCycle
2768
- };
2769
- //# sourceMappingURL=chunk-TTM54LQR.js.map