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
@@ -0,0 +1,1699 @@
1
+ import {
2
+ getXClient
3
+ } from "./chunk-TKGB5LIN.js";
4
+ import {
5
+ addToQueue
6
+ } from "./chunk-AIGSCHZK.js";
7
+ import {
8
+ buildPersonaConstraintLines,
9
+ buildSystemPrompt,
10
+ getPersonaConstraints,
11
+ personaConstraintHandles
12
+ } from "./chunk-73CWOI44.js";
13
+ import {
14
+ listIntents,
15
+ recordIntentExecution
16
+ } from "./chunk-OTZNHIXT.js";
17
+ import {
18
+ updatePostMetrics
19
+ } from "./chunk-ER6TILYZ.js";
20
+ import {
21
+ loadIdentity,
22
+ saveIdentity
23
+ } from "./chunk-IULO3GRE.js";
24
+ import {
25
+ loadCredentials
26
+ } from "./chunk-SXNZVKLJ.js";
27
+ import {
28
+ generateResponse
29
+ } from "./chunk-342ZX72W.js";
30
+ import {
31
+ logger
32
+ } from "./chunk-RSNEVBEI.js";
33
+ import {
34
+ addLearning,
35
+ getRecentInteractions,
36
+ logInteraction
37
+ } from "./chunk-6WBIVXOY.js";
38
+ import {
39
+ paths
40
+ } from "./chunk-ZWKTKWS6.js";
41
+
42
+ // src/runtime/decision-engine.ts
43
+ async function executeAction(action) {
44
+ const { action: type } = action;
45
+ try {
46
+ switch (type) {
47
+ case "post": {
48
+ if (!action.content) return { action: type, success: false, error: "No content provided" };
49
+ if (action.content.length > 280) {
50
+ return { action: type, success: false, error: `Tweet too long: ${action.content.length} chars (max 280)` };
51
+ }
52
+ const client = await getXClient();
53
+ const result = await client.postTweet(action.content);
54
+ if (result.success) logger.info(`Posted: "${action.content.slice(0, 50)}..."`);
55
+ return { action: type, success: result.success, detail: result.tweetId, error: result.error };
56
+ }
57
+ case "reply": {
58
+ if (!action.tweetId || !action.content) {
59
+ return { action: type, success: false, error: "Missing tweetId or content" };
60
+ }
61
+ const client = await getXClient();
62
+ const result = await client.replyToTweet(action.tweetId, action.content);
63
+ if (result.success) logger.info(`Replied to ${action.tweetId}: "${action.content.slice(0, 50)}..."`);
64
+ return { action: type, success: result.success, detail: result.tweetId, error: result.error };
65
+ }
66
+ case "like": {
67
+ if (!action.tweetId) return { action: type, success: false, error: "Missing tweetId" };
68
+ const client = await getXClient();
69
+ const result = await client.likeTweet(action.tweetId);
70
+ return { action: type, success: result.success, error: result.error };
71
+ }
72
+ case "retweet": {
73
+ if (!action.tweetId) return { action: type, success: false, error: "Missing tweetId" };
74
+ const client = await getXClient();
75
+ const result = await client.retweet(action.tweetId);
76
+ return { action: type, success: result.success, error: result.error };
77
+ }
78
+ case "follow": {
79
+ if (!action.handle) return { action: type, success: false, error: "Missing handle" };
80
+ const client = await getXClient();
81
+ const result = await client.followUser(action.handle);
82
+ if (!result.success) {
83
+ logInteraction({
84
+ id: `int-${Date.now()}`,
85
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
86
+ type: "follow",
87
+ targetHandle: action.handle.replace(/^@/, ""),
88
+ creditsUsed: 0,
89
+ success: false,
90
+ error: result.error
91
+ });
92
+ }
93
+ return { action: type, success: result.success, error: result.error };
94
+ }
95
+ case "schedule": {
96
+ if (!action.content) return { action: type, success: false, error: "No content" };
97
+ const entry = addToQueue(action.content);
98
+ logger.info(`Scheduled: "${action.content.slice(0, 50)}..." for ${entry.scheduledFor}`);
99
+ return { action: type, success: true, detail: `Scheduled for ${entry.scheduledFor}` };
100
+ }
101
+ case "learn": {
102
+ if (!action.content) return { action: type, success: false, error: "No content" };
103
+ addLearning(action.content, "agent", action.tags ?? ["heartbeat"]);
104
+ logger.info(`Learned: "${action.content.slice(0, 50)}..."`);
105
+ return { action: type, success: true };
106
+ }
107
+ case "reflect": {
108
+ if (!action.content) return { action: type, success: false, error: "No content" };
109
+ const identity = loadIdentity();
110
+ identity.evolutionJournal.push({
111
+ date: (/* @__PURE__ */ new Date()).toISOString(),
112
+ reflection: action.content
113
+ });
114
+ saveIdentity(identity);
115
+ logger.info(`Reflected: "${action.content.slice(0, 50)}..."`);
116
+ return { action: type, success: true };
117
+ }
118
+ case "skip": {
119
+ logger.info(`Skipping: ${action.reason ?? action.reasoning ?? "no reason given"}`);
120
+ return { action: type, success: true, detail: action.reason ?? action.reasoning };
121
+ }
122
+ default:
123
+ logger.warn(`Unknown action: ${type}`);
124
+ return { action: type, success: false, error: `Unknown action: ${type}` };
125
+ }
126
+ } catch (error) {
127
+ const msg = error.message;
128
+ logger.error(`Action ${type} failed: ${msg}`);
129
+ return { action: type, success: false, error: msg };
130
+ }
131
+ }
132
+
133
+ // src/runtime/policy.ts
134
+ function normalize(text) {
135
+ return text.toLowerCase().replace(/https?:\/\/\S+/g, "").replace(/[@#]\w+/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
136
+ }
137
+ function tokenSet(text) {
138
+ const tokens = normalize(text).split(" ").filter(Boolean);
139
+ return new Set(tokens);
140
+ }
141
+ function jaccardSimilarity(a, b) {
142
+ const aSet = tokenSet(a);
143
+ const bSet = tokenSet(b);
144
+ if (aSet.size === 0 || bSet.size === 0) return 0;
145
+ let overlap = 0;
146
+ for (const token of aSet) {
147
+ if (bSet.has(token)) overlap += 1;
148
+ }
149
+ const union = aSet.size + bSet.size - overlap;
150
+ return union === 0 ? 0 : overlap / union;
151
+ }
152
+ function firstWords(text, n) {
153
+ return normalize(text).split(" ").filter(Boolean).slice(0, n).join(" ");
154
+ }
155
+ function hasAllCapsEnding(text) {
156
+ const ending = text.split(/[.!?]/).map((s) => s.trim()).filter(Boolean).slice(-1)[0] ?? "";
157
+ const words = ending.split(/\s+/).filter(Boolean);
158
+ if (words.length < 3) return false;
159
+ return words.every((word) => /^[A-Z0-9]+$/.test(word));
160
+ }
161
+ function wordCount(text) {
162
+ return normalize(text).split(" ").filter(Boolean).length;
163
+ }
164
+ function sentenceCount(text) {
165
+ return text.split(/[.!?]/).map((part) => part.trim()).filter(Boolean).length;
166
+ }
167
+ function explanatoryMarkerHits(text) {
168
+ const lower = text.toLowerCase();
169
+ const markers = [
170
+ "the real issue",
171
+ "the core issue",
172
+ "in other words",
173
+ "the point is",
174
+ "what this means",
175
+ "the lesson",
176
+ "therefore",
177
+ "ultimately",
178
+ "meanwhile",
179
+ "this reveals",
180
+ "this proves"
181
+ ];
182
+ return markers.reduce((hits, marker) => hits + (lower.includes(marker) ? 1 : 0), 0);
183
+ }
184
+ function soundsLikeLecture(content) {
185
+ const words = wordCount(content);
186
+ const commas = (content.match(/,/g) ?? []).length;
187
+ const semicolons = (content.match(/;/g) ?? []).length;
188
+ const hasQuestion = content.includes("?");
189
+ const markerHits = explanatoryMarkerHits(content);
190
+ if (words > 58) return true;
191
+ if (words > 42 && commas + semicolons >= 3 && !hasQuestion) return true;
192
+ if (markerHits >= 2) return true;
193
+ return false;
194
+ }
195
+ function soundsTooHedgedForPersona(content) {
196
+ try {
197
+ const identity = loadIdentity();
198
+ const assertive = identity.traits.confidence >= 0.72 || identity.traits.aggression >= 0.62 || identity.conflictStyle === "clap-back" || identity.conflictStyle === "debate";
199
+ if (!assertive) return false;
200
+ const lower = content.toLowerCase();
201
+ const hedges = [
202
+ "you're both right",
203
+ "both can be true",
204
+ "on one hand",
205
+ "on the other hand",
206
+ "it depends",
207
+ "it's complicated",
208
+ "nuance",
209
+ "fair point from both"
210
+ ];
211
+ return hedges.some((phrase) => lower.includes(phrase));
212
+ } catch {
213
+ return false;
214
+ }
215
+ }
216
+ function allowsPhilosopherCadence() {
217
+ try {
218
+ const identity = loadIdentity();
219
+ if (identity.framework === "philosopher") return true;
220
+ if (identity.framework === "truthseeker") return false;
221
+ return false;
222
+ } catch {
223
+ return false;
224
+ }
225
+ }
226
+ function philosophicalPatternHits(content) {
227
+ const lower = content.toLowerCase();
228
+ const markers = [
229
+ "the real question",
230
+ "the deeper question",
231
+ "what becomes possible",
232
+ "is just the start",
233
+ "the future isn't about",
234
+ "the future is not about",
235
+ "when intelligence becomes",
236
+ "protocols are infrastructure",
237
+ "the core future",
238
+ "the true question",
239
+ "value might emerge",
240
+ "isn't about",
241
+ "is not about",
242
+ "it's about",
243
+ "just the start"
244
+ ];
245
+ return markers.reduce((hits, marker) => hits + (lower.includes(marker) ? 1 : 0), 0);
246
+ }
247
+ function abstractWordCount(content) {
248
+ const words = normalize(content).split(" ").filter(Boolean);
249
+ const abstractWords = /* @__PURE__ */ new Set([
250
+ "infrastructure",
251
+ "protocol",
252
+ "protocols",
253
+ "architecture",
254
+ "architectures",
255
+ "consciousness",
256
+ "economy",
257
+ "economics",
258
+ "currency",
259
+ "systems",
260
+ "system",
261
+ "future",
262
+ "intelligence",
263
+ "optimization",
264
+ "abstraction",
265
+ "paradigm",
266
+ "emerge",
267
+ "emergence",
268
+ "governs",
269
+ "governance"
270
+ ]);
271
+ return words.filter((word) => abstractWords.has(word)).length;
272
+ }
273
+ function usesManifestoPunctuation(content) {
274
+ return content.includes(":") || content.includes(";") || content.includes("\u2014");
275
+ }
276
+ var OVERLAP_STOPWORDS = /* @__PURE__ */ new Set([
277
+ "this",
278
+ "that",
279
+ "with",
280
+ "from",
281
+ "have",
282
+ "what",
283
+ "when",
284
+ "where",
285
+ "which",
286
+ "your",
287
+ "youre",
288
+ "they",
289
+ "them",
290
+ "about",
291
+ "into",
292
+ "over",
293
+ "under",
294
+ "while",
295
+ "just",
296
+ "like",
297
+ "really",
298
+ "there",
299
+ "their",
300
+ "then",
301
+ "than",
302
+ "because",
303
+ "would",
304
+ "could",
305
+ "should",
306
+ "being",
307
+ "been",
308
+ "it's",
309
+ "its",
310
+ "the",
311
+ "and",
312
+ "for",
313
+ "are",
314
+ "was",
315
+ "were",
316
+ "will",
317
+ "not",
318
+ "but",
319
+ "you",
320
+ "our",
321
+ "out",
322
+ "all",
323
+ "too",
324
+ "can",
325
+ "get",
326
+ "got",
327
+ "any"
328
+ ]);
329
+ function meaningfulTokenSet(text) {
330
+ const tokens = normalize(text).split(" ").filter((token) => token.length >= 4 && !OVERLAP_STOPWORDS.has(token));
331
+ return new Set(tokens);
332
+ }
333
+ function hasMeaningfulContextOverlap(content, targetText) {
334
+ const a = meaningfulTokenSet(content);
335
+ const b = meaningfulTokenSet(targetText);
336
+ if (a.size === 0 || b.size === 0) return false;
337
+ for (const token of a) {
338
+ if (b.has(token)) return true;
339
+ }
340
+ return false;
341
+ }
342
+ function recentWrittenContent() {
343
+ const recent = getRecentInteractions(40);
344
+ return recent.filter((i) => i.type === "post" || i.type === "reply").map((i) => i.content ?? "").filter((content) => content.length > 0);
345
+ }
346
+ var REPETITION_STOPWORDS = /* @__PURE__ */ new Set([
347
+ "about",
348
+ "after",
349
+ "again",
350
+ "against",
351
+ "almost",
352
+ "always",
353
+ "among",
354
+ "because",
355
+ "being",
356
+ "between",
357
+ "could",
358
+ "every",
359
+ "first",
360
+ "found",
361
+ "from",
362
+ "going",
363
+ "great",
364
+ "have",
365
+ "having",
366
+ "here",
367
+ "into",
368
+ "just",
369
+ "like",
370
+ "make",
371
+ "more",
372
+ "most",
373
+ "much",
374
+ "only",
375
+ "other",
376
+ "over",
377
+ "really",
378
+ "same",
379
+ "some",
380
+ "such",
381
+ "than",
382
+ "that",
383
+ "their",
384
+ "there",
385
+ "these",
386
+ "they",
387
+ "this",
388
+ "those",
389
+ "through",
390
+ "time",
391
+ "very",
392
+ "what",
393
+ "when",
394
+ "where",
395
+ "which",
396
+ "while",
397
+ "with",
398
+ "would",
399
+ "your",
400
+ "youre",
401
+ "dont",
402
+ "cant",
403
+ "will",
404
+ "shall",
405
+ "should",
406
+ "also",
407
+ "tweet",
408
+ "tweets",
409
+ "thread",
410
+ "threads",
411
+ "future",
412
+ "human",
413
+ "humans",
414
+ "technology",
415
+ "tech",
416
+ "build",
417
+ "building",
418
+ "system",
419
+ "systems",
420
+ "agent",
421
+ "agents"
422
+ ]);
423
+ function informativeTokens(text) {
424
+ return normalize(text).split(" ").filter((token) => token.length >= 5 && !REPETITION_STOPWORDS.has(token));
425
+ }
426
+ function findOverusedAnchor(content, recent) {
427
+ const recentWindow = recent.slice(0, 12);
428
+ if (recentWindow.length < 5) return null;
429
+ const tokenCounts = /* @__PURE__ */ new Map();
430
+ const bigramCounts = /* @__PURE__ */ new Map();
431
+ for (const sample of recentWindow) {
432
+ const tokens = informativeTokens(sample);
433
+ const uniqueTokens = new Set(tokens);
434
+ for (const token of uniqueTokens) {
435
+ tokenCounts.set(token, (tokenCounts.get(token) ?? 0) + 1);
436
+ }
437
+ for (let i = 0; i < tokens.length - 1; i += 1) {
438
+ const bigram = `${tokens[i]} ${tokens[i + 1]}`;
439
+ bigramCounts.set(bigram, (bigramCounts.get(bigram) ?? 0) + 1);
440
+ }
441
+ }
442
+ const contentTokens = informativeTokens(content);
443
+ const contentTokenSet = new Set(contentTokens);
444
+ for (const token of contentTokenSet) {
445
+ const count = tokenCounts.get(token) ?? 0;
446
+ if (count >= 4) return token;
447
+ }
448
+ for (let i = 0; i < contentTokens.length - 1; i += 1) {
449
+ const bigram = `${contentTokens[i]} ${contentTokens[i + 1]}`;
450
+ const count = bigramCounts.get(bigram) ?? 0;
451
+ if (count >= 3) return bigram;
452
+ }
453
+ return null;
454
+ }
455
+ function openingFragment(text, words = 4) {
456
+ return normalize(text).split(" ").filter(Boolean).slice(0, words).join(" ");
457
+ }
458
+ function findOverusedOpening(content, recent) {
459
+ const opening = openingFragment(content, 4);
460
+ if (!opening || opening.split(" ").length < 3) return null;
461
+ let matches = 0;
462
+ for (const sample of recent.slice(0, 12)) {
463
+ if (openingFragment(sample, 4) === opening) {
464
+ matches += 1;
465
+ }
466
+ }
467
+ return matches >= 2 ? opening : null;
468
+ }
469
+ function vocabularyNoveltyRatio(content, recent) {
470
+ const current = new Set(informativeTokens(content));
471
+ if (current.size === 0) return 1;
472
+ const seen = /* @__PURE__ */ new Set();
473
+ for (const sample of recent.slice(0, 14)) {
474
+ for (const token of informativeTokens(sample)) {
475
+ seen.add(token);
476
+ }
477
+ }
478
+ let fresh = 0;
479
+ for (const token of current) {
480
+ if (!seen.has(token)) fresh += 1;
481
+ }
482
+ return fresh / current.size;
483
+ }
484
+ function looksOverAbstractWithoutAnchor(content) {
485
+ const abstractCount = abstractWordCount(content);
486
+ if (abstractCount < 4) return false;
487
+ const words = wordCount(content);
488
+ const density = abstractCount / Math.max(1, words);
489
+ if (density < 0.2) return false;
490
+ const lower = content.toLowerCase();
491
+ const hasHandle = /@\w+/.test(content);
492
+ const hasNumber = /\b\d+(\.\d+)?\b/.test(content);
493
+ const hasQuote = /["“”']/.test(content);
494
+ const hasSpecificFigure = /\b(musk|elon|zuck|altman|openai|tesla|spacex|meta|google|apple)\b/.test(lower);
495
+ const hasQuestion = content.includes("?");
496
+ return !(hasHandle || hasNumber || hasQuote || hasSpecificFigure || hasQuestion);
497
+ }
498
+ function hasStrongConversationOpportunity(timeline, mentions) {
499
+ if (mentions.length > 0) return true;
500
+ return timeline.some((tweet) => (tweet.replyCount ?? 0) > 0 || tweet.text.includes("?"));
501
+ }
502
+ function wasInteractionAction(action) {
503
+ return ["reply", "like", "retweet", "follow"].includes(action.action);
504
+ }
505
+ function isWritingAction(action) {
506
+ return ["post", "reply", "schedule"].includes(action.action);
507
+ }
508
+ function normalizeHandle(handle) {
509
+ return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
510
+ }
511
+ function resolveActionTargetHandle(action, tweetById) {
512
+ if (action.handle) return normalizeHandle(action.handle);
513
+ if (action.targetHandle) return normalizeHandle(action.targetHandle);
514
+ if (action.tweetId) return normalizeHandle(tweetById.get(action.tweetId)?.authorHandle);
515
+ return "";
516
+ }
517
+ function executedWrittenContent(executedActions) {
518
+ return executedActions.filter((a) => isWritingAction(a) && typeof a.content === "string").map((a) => a.content?.trim() ?? "").filter((content) => content.length > 0);
519
+ }
520
+ function nearExactDuplicate(content, recent) {
521
+ const normalized = normalize(content);
522
+ if (!normalized) return false;
523
+ return recent.some((r) => {
524
+ const candidate = normalize(r);
525
+ if (!candidate) return false;
526
+ if (candidate === normalized) return true;
527
+ return jaccardSimilarity(content, r) >= 0.88;
528
+ });
529
+ }
530
+ function isDuplicateTarget(action, executedActions) {
531
+ if (!action.tweetId) return false;
532
+ return executedActions.some((a) => a.tweetId === action.tweetId && a.action === action.action);
533
+ }
534
+ function repeatedTemplate(content, recent) {
535
+ const prefix = firstWords(content, 7);
536
+ if (!prefix) return false;
537
+ return recent.some((r) => {
538
+ const sameStart = firstWords(r, 7) === prefix;
539
+ const similar = jaccardSimilarity(content, r) >= 0.62;
540
+ return sameStart || similar;
541
+ });
542
+ }
543
+ function overusingAllCapsCadence(content, recentEntries) {
544
+ if (!hasAllCapsEnding(content)) return false;
545
+ const recentCaps = recentEntries.filter((i) => i.type === "post" && i.content).slice(0, 8).filter((i) => hasAllCapsEnding(i.content ?? ""));
546
+ return recentCaps.length >= 2;
547
+ }
548
+ function evaluateActionPolicy(context) {
549
+ const {
550
+ action,
551
+ step,
552
+ timeline,
553
+ mentions,
554
+ executedActions,
555
+ observedTweetIds,
556
+ observedTweets,
557
+ selfHandle,
558
+ selfUserId
559
+ } = context;
560
+ const self = normalizeHandle(selfHandle);
561
+ const selfId = (selfUserId ?? "").trim();
562
+ const knownTweets = observedTweets ?? [...timeline, ...mentions];
563
+ const tweetById = new Map(knownTweets.map((tweet) => [tweet.id, tweet]));
564
+ const constraints = getPersonaConstraints();
565
+ const strictReplyHandles = new Set(constraints.onlyReplyToHandles.map((handle) => normalizeHandle(handle)));
566
+ const strictInteractHandles = new Set(personaConstraintHandles(constraints).map((handle) => normalizeHandle(handle)));
567
+ const targetHandle = resolveActionTargetHandle(action, tweetById);
568
+ if (constraints.replyOnlyMode && !["reply", "skip", "learn", "reflect"].includes(action.action)) {
569
+ return {
570
+ allowed: false,
571
+ reason: "Persona is in reply-only mode. Use reply actions only."
572
+ };
573
+ }
574
+ if (constraints.noOriginalPosts && (action.action === "post" || action.action === "schedule")) {
575
+ return {
576
+ allowed: false,
577
+ reason: "Persona forbids standalone original posts."
578
+ };
579
+ }
580
+ if (strictReplyHandles.size > 0) {
581
+ if (action.action !== "reply") {
582
+ return {
583
+ allowed: false,
584
+ reason: `Persona requires replying only to specific target(s): @${[...strictReplyHandles].join(", @")}.`
585
+ };
586
+ }
587
+ if (!targetHandle || !strictReplyHandles.has(targetHandle)) {
588
+ return {
589
+ allowed: false,
590
+ reason: `Reply target must be one of @${[...strictReplyHandles].join(", @")}.`
591
+ };
592
+ }
593
+ }
594
+ if (strictInteractHandles.size > 0 && ["reply", "like", "retweet", "follow"].includes(action.action)) {
595
+ if (!targetHandle || !strictInteractHandles.has(targetHandle)) {
596
+ return {
597
+ allowed: false,
598
+ reason: `Interaction target must be one of @${[...strictInteractHandles].join(", @")}.`
599
+ };
600
+ }
601
+ }
602
+ if (isDuplicateTarget(action, executedActions)) {
603
+ return { allowed: false, reason: `Action ${action.action} already executed for tweet ${action.tweetId} this heartbeat.` };
604
+ }
605
+ if (action.content && isWritingAction(action)) {
606
+ if (action.content.length > 280) {
607
+ return {
608
+ allowed: false,
609
+ reason: `Content is too long (${action.content.length}/280). Keep it tighter and more conversational.`
610
+ };
611
+ }
612
+ if (action.action === "reply" && wordCount(action.content) > 45 && !action.content.includes("?")) {
613
+ return {
614
+ allowed: false,
615
+ reason: "Reply is too monologue-like. Keep it shorter or ask a direct question."
616
+ };
617
+ }
618
+ if (soundsLikeLecture(action.content)) {
619
+ return {
620
+ allowed: false,
621
+ reason: "Rejected lecture-like writing style. Use a more human, reactive, conversational tone."
622
+ };
623
+ }
624
+ if (soundsTooHedgedForPersona(action.content)) {
625
+ return {
626
+ allowed: false,
627
+ reason: "Rejected hedged consensus phrasing. Your persona should be more decisive and opinionated."
628
+ };
629
+ }
630
+ if (!allowsPhilosopherCadence()) {
631
+ const phiHits = philosophicalPatternHits(action.content);
632
+ const abstractCount = abstractWordCount(action.content);
633
+ const hasQuestion = action.content.includes("?");
634
+ if (phiHits >= 1 || abstractCount >= 5 && hasQuestion) {
635
+ return {
636
+ allowed: false,
637
+ reason: "Rejected abstract philosopher cadence. Use concrete, contextual language tied to the actual tweet."
638
+ };
639
+ }
640
+ const words = wordCount(action.content);
641
+ const sentences = sentenceCount(action.content);
642
+ if (action.action === "reply" && (words > 34 || sentences > 2)) {
643
+ return {
644
+ allowed: false,
645
+ reason: "Reply is too long/explanatory for this persona. Keep it short and direct."
646
+ };
647
+ }
648
+ if (action.action === "post" && (words > 42 || sentences > 3)) {
649
+ return {
650
+ allowed: false,
651
+ reason: "Post is drifting into manifesto style. Keep it more concrete and human."
652
+ };
653
+ }
654
+ if ((action.action === "reply" || action.action === "post") && usesManifestoPunctuation(action.content) && words > 16) {
655
+ return {
656
+ allowed: false,
657
+ reason: "Rejected manifesto punctuation style. Avoid colon/semicolon/em-dash framing."
658
+ };
659
+ }
660
+ }
661
+ const existingInHeartbeat = executedWrittenContent(executedActions);
662
+ if (nearExactDuplicate(action.content, existingInHeartbeat)) {
663
+ return {
664
+ allowed: false,
665
+ reason: "Rejected duplicate wording in this heartbeat. Write a distinct message before posting/replying again."
666
+ };
667
+ }
668
+ const recent = recentWrittenContent();
669
+ const overusedAnchor = findOverusedAnchor(action.content, recent);
670
+ if (overusedAnchor) {
671
+ return {
672
+ allowed: false,
673
+ reason: `Rejected repetitive anchor phrase/term "${overusedAnchor}". Use a different framing and vocabulary.`
674
+ };
675
+ }
676
+ const overusedOpening = findOverusedOpening(action.content, recent);
677
+ if (overusedOpening) {
678
+ return {
679
+ allowed: false,
680
+ reason: `Rejected repetitive opening phrase "${overusedOpening}". Start from a fresh angle.`
681
+ };
682
+ }
683
+ if (recent.length >= 6 && wordCount(action.content) >= 10) {
684
+ const novelty = vocabularyNoveltyRatio(action.content, recent);
685
+ if (novelty < 0.34) {
686
+ return {
687
+ allowed: false,
688
+ reason: "Rejected low-novelty vocabulary loop. Use fresher words, examples, or a different conversational angle."
689
+ };
690
+ }
691
+ }
692
+ if (!allowsPhilosopherCadence() && looksOverAbstractWithoutAnchor(action.content)) {
693
+ return {
694
+ allowed: false,
695
+ reason: "Rejected abstract wording without concrete anchor. Reference a specific person, event, quote, or question."
696
+ };
697
+ }
698
+ }
699
+ const hasConversationOpportunity = hasStrongConversationOpportunity(timeline, mentions);
700
+ const interactedAlready = executedActions.some(wasInteractionAction);
701
+ const observedCount = observedTweetIds && observedTweetIds.length > 0 ? observedTweetIds.length : timeline.length + mentions.length;
702
+ if ((action.action === "reply" || action.action === "like" || action.action === "retweet") && observedCount === 0) {
703
+ return {
704
+ allowed: false,
705
+ reason: "No tweets observed in current context. Avoid blind interactions; gather timeline/search context first."
706
+ };
707
+ }
708
+ if (action.action === "post" && hasConversationOpportunity && !interactedAlready && step < 2) {
709
+ return {
710
+ allowed: false,
711
+ reason: "Engage first: reply/like/follow when mentions or active conversations are available before posting an original tweet."
712
+ };
713
+ }
714
+ if (isWritingAction(action) && action.content) {
715
+ const recent = recentWrittenContent();
716
+ if (nearExactDuplicate(action.content, recent)) {
717
+ return {
718
+ allowed: false,
719
+ reason: "Rejected near-duplicate content from recent history. Tailor this message to the specific context."
720
+ };
721
+ }
722
+ if (repeatedTemplate(action.content, recent)) {
723
+ return {
724
+ allowed: false,
725
+ reason: "Rejected repetitive content pattern. Use a more novel structure or engage directly with timeline context."
726
+ };
727
+ }
728
+ }
729
+ if ((action.action === "post" || action.action === "schedule") && action.content) {
730
+ const recentInteractions = getRecentInteractions(20);
731
+ if (overusingAllCapsCadence(action.content, recentInteractions)) {
732
+ return {
733
+ allowed: false,
734
+ reason: "Rejected repetitive all-caps slogan cadence. Vary style and reduce monologue formatting."
735
+ };
736
+ }
737
+ }
738
+ if ((action.action === "reply" || action.action === "like" || action.action === "retweet") && action.tweetId) {
739
+ const known = new Set(observedTweetIds ?? [...timeline, ...mentions].map((tweet) => tweet.id));
740
+ if (!known.has(action.tweetId)) {
741
+ return {
742
+ allowed: false,
743
+ reason: `Tweet ${action.tweetId} is not in current observations. Choose a visible timeline/mention tweet for grounded engagement.`
744
+ };
745
+ }
746
+ const targetTweet = tweetById.get(action.tweetId);
747
+ if (targetTweet && (self && normalizeHandle(targetTweet.authorHandle) === self || selfId && targetTweet.authorId === selfId)) {
748
+ return {
749
+ allowed: false,
750
+ reason: "Rejected self-interaction. Do not reply/like/retweet your own tweets."
751
+ };
752
+ }
753
+ }
754
+ if (action.action === "reply" && action.tweetId) {
755
+ if (action.content) {
756
+ const targetTweet = tweetById.get(action.tweetId);
757
+ if (targetTweet && !hasMeaningfulContextOverlap(action.content, targetTweet.text)) {
758
+ const phiHits = philosophicalPatternHits(action.content);
759
+ if (wordCount(action.content) > 9 || phiHits > 0 || abstractWordCount(action.content) >= 4) {
760
+ return {
761
+ allowed: false,
762
+ reason: "Reply is not grounded in the target tweet context. Reference concrete details from their actual post."
763
+ };
764
+ }
765
+ }
766
+ }
767
+ }
768
+ if (action.action === "follow" && action.handle) {
769
+ const target = normalizeHandle(action.handle);
770
+ if (!target) {
771
+ return { allowed: false, reason: "Follow target is empty." };
772
+ }
773
+ if (self && target === self) {
774
+ return {
775
+ allowed: false,
776
+ reason: "Rejected self-follow. Do not follow your own account."
777
+ };
778
+ }
779
+ }
780
+ return { allowed: true };
781
+ }
782
+
783
+ // src/runtime/bandit.ts
784
+ import { existsSync, readFileSync, writeFileSync } from "fs";
785
+ var DEFAULT_EXPLORATION_BUDGET = 0.25;
786
+ function defaultState() {
787
+ return {
788
+ totalPulls: 0,
789
+ explorationBudget: DEFAULT_EXPLORATION_BUDGET,
790
+ arms: {},
791
+ pending: []
792
+ };
793
+ }
794
+ function clamp(value, min, max) {
795
+ return Math.max(min, Math.min(max, value));
796
+ }
797
+ function loadState() {
798
+ if (!existsSync(paths.bandit)) return defaultState();
799
+ try {
800
+ const parsed = JSON.parse(readFileSync(paths.bandit, "utf-8"));
801
+ const state = {
802
+ totalPulls: Math.max(0, parsed.totalPulls ?? 0),
803
+ explorationBudget: clamp(parsed.explorationBudget ?? DEFAULT_EXPLORATION_BUDGET, 0.1, 0.45),
804
+ arms: parsed.arms ?? {},
805
+ pending: Array.isArray(parsed.pending) ? parsed.pending : []
806
+ };
807
+ return state;
808
+ } catch {
809
+ return defaultState();
810
+ }
811
+ }
812
+ function saveState(state) {
813
+ state.explorationBudget = clamp(state.explorationBudget, 0.1, 0.45);
814
+ state.pending = state.pending.filter((entry) => {
815
+ if (!entry.resolved) return true;
816
+ const resolvedAtMs = Date.parse(entry.resolveAt);
817
+ if (Number.isNaN(resolvedAtMs)) return false;
818
+ return Date.now() - resolvedAtMs < 7 * 24 * 60 * 60 * 1e3;
819
+ }).slice(-1200);
820
+ writeFileSync(paths.bandit, JSON.stringify(state, null, 2));
821
+ }
822
+ function ensureArm(state, armKey) {
823
+ const existing = state.arms[armKey];
824
+ if (existing) return existing;
825
+ const arm = {
826
+ key: armKey,
827
+ pulls: 0,
828
+ rewardSum: 0,
829
+ lastReward: 0,
830
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
831
+ };
832
+ state.arms[armKey] = arm;
833
+ return arm;
834
+ }
835
+ function computeImmediateReward(result) {
836
+ if (result.success) return 0.12;
837
+ const error = (result.error ?? "").toLowerCase();
838
+ if (error.includes("duplicate content")) return -0.95;
839
+ if (error.includes("rate limit")) return -0.5;
840
+ if (error.includes("forbidden") || error.includes("403")) return -0.55;
841
+ return -0.35;
842
+ }
843
+ function resolveDelayedReward(metrics) {
844
+ if (!metrics) return -0.05;
845
+ 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);
846
+ if (metrics.likeCount === 0 && metrics.replyCount === 0 && metrics.retweetCount === 0) {
847
+ return -0.15;
848
+ }
849
+ return Math.min(reward, 1.8);
850
+ }
851
+ function armKeyFromAction(action) {
852
+ if (action.banditArm && action.banditArm.trim().length > 0) return action.banditArm;
853
+ const source = action.source ?? "unknown";
854
+ return `${action.action}:${source}`;
855
+ }
856
+ function extractTweetId(action, result) {
857
+ if (result.detail && /^\d+$/.test(result.detail)) return result.detail;
858
+ if (action.action !== "reply" && action.tweetId) return action.tweetId;
859
+ return void 0;
860
+ }
861
+ function recordBanditActionResults(actions, results) {
862
+ if (actions.length === 0 || results.length === 0) return;
863
+ const state = loadState();
864
+ const now = (/* @__PURE__ */ new Date()).toISOString();
865
+ for (let i = 0; i < Math.min(actions.length, results.length); i += 1) {
866
+ const action = actions[i];
867
+ const result = results[i];
868
+ const armKey = armKeyFromAction(action);
869
+ const arm = ensureArm(state, armKey);
870
+ const immediateReward = computeImmediateReward(result);
871
+ arm.pulls += 1;
872
+ arm.rewardSum += immediateReward;
873
+ arm.lastReward = immediateReward;
874
+ arm.updatedAt = now;
875
+ state.totalPulls += 1;
876
+ const tweetId = extractTweetId(action, result);
877
+ if (result.success && tweetId && (action.action === "post" || action.action === "reply")) {
878
+ const delayMs = action.action === "reply" ? 20 * 60 * 1e3 : 45 * 60 * 1e3;
879
+ state.pending.push({
880
+ id: `pending-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
881
+ armKey,
882
+ actionType: action.action,
883
+ tweetId,
884
+ createdAt: now,
885
+ resolveAt: new Date(Date.now() + delayMs).toISOString(),
886
+ resolved: false
887
+ });
888
+ }
889
+ }
890
+ saveState(state);
891
+ }
892
+ async function collectDelayedOutcomes(client) {
893
+ const state = loadState();
894
+ const now = Date.now();
895
+ const pendingDue = state.pending.filter((entry) => !entry.resolved && Date.parse(entry.resolveAt) <= now).slice(0, 8);
896
+ if (pendingDue.length === 0) {
897
+ return { processed: 0, resolved: 0, rewardAdded: 0 };
898
+ }
899
+ let rewardAdded = 0;
900
+ let resolved = 0;
901
+ const checkedAt = (/* @__PURE__ */ new Date()).toISOString();
902
+ for (const pending of pendingDue) {
903
+ const arm = ensureArm(state, pending.armKey);
904
+ let metrics = null;
905
+ if (pending.tweetId) {
906
+ metrics = await client.getTweetMetrics(pending.tweetId);
907
+ if (metrics) {
908
+ updatePostMetrics(pending.tweetId, {
909
+ checkedAt,
910
+ likes: metrics.likeCount,
911
+ retweets: metrics.retweetCount,
912
+ replies: metrics.replyCount
913
+ });
914
+ }
915
+ }
916
+ const reward = resolveDelayedReward(
917
+ metrics ? {
918
+ likeCount: metrics.likeCount,
919
+ retweetCount: metrics.retweetCount,
920
+ replyCount: metrics.replyCount
921
+ } : null
922
+ );
923
+ arm.rewardSum += reward;
924
+ arm.lastReward = reward;
925
+ arm.updatedAt = checkedAt;
926
+ rewardAdded += reward;
927
+ pending.resolved = true;
928
+ resolved += 1;
929
+ }
930
+ saveState(state);
931
+ logger.info(
932
+ `Bandit delayed outcomes processed=${pendingDue.length}, resolved=${resolved}, rewardDelta=${rewardAdded.toFixed(2)}`
933
+ );
934
+ return {
935
+ processed: pendingDue.length,
936
+ resolved,
937
+ rewardAdded
938
+ };
939
+ }
940
+
941
+ // src/runtime/autonomy.ts
942
+ function selfUserIdFromCredentials() {
943
+ try {
944
+ const creds = loadCredentials();
945
+ const accessToken = creds.accessToken;
946
+ if (!accessToken) return null;
947
+ const dashIdx = accessToken.indexOf("-");
948
+ if (dashIdx <= 0) return null;
949
+ const candidate = accessToken.substring(0, dashIdx);
950
+ return /^\d+$/.test(candidate) ? candidate : null;
951
+ } catch {
952
+ return null;
953
+ }
954
+ }
955
+ function normalizeHandle2(handle) {
956
+ return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
957
+ }
958
+ function canonicalPlannerHandle(handle) {
959
+ const clean = normalizeHandle2(handle);
960
+ const aliasMap = {
961
+ gork: "grok",
962
+ spora: "sporaai",
963
+ spora_ai: "sporaai"
964
+ };
965
+ return aliasMap[clean] ?? clean;
966
+ }
967
+ function canonicalizePlannerQuery(rawQuery) {
968
+ return rawQuery.replace(/from:([a-zA-Z0-9_]{1,15})/gi, (_match, handle) => `from:${canonicalPlannerHandle(handle)}`);
969
+ }
970
+ function roughWordCount(text) {
971
+ return text.trim().split(/\s+/).filter(Boolean).length;
972
+ }
973
+ function inferStyleMode(text) {
974
+ const lower = text.toLowerCase();
975
+ if (text.includes("?")) return "curious_question";
976
+ if (/\b(lol|lmao|haha|wild|fr|ngl)\b/i.test(text)) return "playful_line";
977
+ if (/\b(not|isn'?t|wrong|nah|nope|doesn'?t)\b/i.test(lower)) return "friendly_pushback";
978
+ if (roughWordCount(text) <= 9) return "quick_reaction";
979
+ return "plain_observation";
980
+ }
981
+ function chooseRewriteStyleMode(recentTexts, targetTweetText) {
982
+ const counts = {
983
+ quick_reaction: 0,
984
+ curious_question: 0,
985
+ friendly_pushback: 0,
986
+ plain_observation: 0,
987
+ playful_line: 0
988
+ };
989
+ for (const text of (recentTexts ?? []).slice(0, 10)) {
990
+ counts[inferStyleMode(text)] += 1;
991
+ }
992
+ if ((targetTweetText ?? "").includes("?") && counts.curious_question <= 2) {
993
+ return "curious_question";
994
+ }
995
+ return Object.entries(counts).sort((a, b) => a[1] - b[1])[0]?.[0] ?? "plain_observation";
996
+ }
997
+ function rewriteStyleModeInstructions(mode) {
998
+ if (mode === "quick_reaction") {
999
+ return [
1000
+ "- style mode: quick reaction",
1001
+ "- 6-14 words, one short sentence",
1002
+ "- immediate reaction tone, no thesis statement"
1003
+ ];
1004
+ }
1005
+ if (mode === "curious_question") {
1006
+ return [
1007
+ "- style mode: curious question",
1008
+ "- include exactly one direct question",
1009
+ "- ask about a specific detail from the target tweet"
1010
+ ];
1011
+ }
1012
+ if (mode === "friendly_pushback") {
1013
+ return [
1014
+ "- style mode: friendly pushback",
1015
+ "- politely disagree in plain words",
1016
+ "- no lecture or abstract framing"
1017
+ ];
1018
+ }
1019
+ if (mode === "playful_line") {
1020
+ return [
1021
+ "- style mode: playful line",
1022
+ "- keep it light and witty, not sarcastic essay mode",
1023
+ "- avoid internet clich\xE9s and overused slogans"
1024
+ ];
1025
+ }
1026
+ return [
1027
+ "- style mode: plain observation",
1028
+ "- write like a natural human thought said out loud",
1029
+ "- concrete detail first, no broad generalization"
1030
+ ];
1031
+ }
1032
+ function shouldAttemptStyleRewrite(action, reason) {
1033
+ if (!action.content || action.action !== "reply" && action.action !== "post") return false;
1034
+ const lower = reason.toLowerCase();
1035
+ 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");
1036
+ }
1037
+ function cleanRewriteOutput(text) {
1038
+ const cleaned = text.replace(/```[\s\S]*?```/g, "").replace(/^["'`]+|["'`]+$/g, "").trim();
1039
+ const firstLine = cleaned.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? cleaned;
1040
+ return firstLine.trim();
1041
+ }
1042
+ async function rewriteDraftForHumanVoice(input) {
1043
+ if (!input.action.content) return null;
1044
+ const identity = loadIdentity();
1045
+ const maxChars = input.action.action === "reply" ? 100 : 130;
1046
+ const minChars = input.action.action === "reply" ? 18 : 24;
1047
+ const styleMode = chooseRewriteStyleMode(input.recentTexts, input.targetTweetText);
1048
+ const system = [
1049
+ `You rewrite X/Twitter drafts into ${identity.name}'s natural voice.`,
1050
+ "Output must feel human, direct, and contextual.",
1051
+ "Never write abstract philosophy or explanatory manifesto style.",
1052
+ "Return ONLY rewritten tweet text, no commentary."
1053
+ ].join(" ");
1054
+ const promptParts = [];
1055
+ promptParts.push(`Current draft: ${input.action.content}`);
1056
+ promptParts.push(`Rejected because: ${input.reason}`);
1057
+ if (input.targetTweetText) {
1058
+ promptParts.push(`Target tweet context: ${input.targetTweetText}`);
1059
+ }
1060
+ promptParts.push(`Constraints:`);
1061
+ promptParts.push(`- ${minChars}-${maxChars} characters`);
1062
+ promptParts.push("- 1-2 short sentences max");
1063
+ promptParts.push("- no 'the real question' / 'the deeper question' framing");
1064
+ promptParts.push("- no colon, semicolon, or em dash");
1065
+ promptParts.push("- specific and concrete, not abstract");
1066
+ promptParts.push("- keep the same core stance");
1067
+ promptParts.push("- use natural spoken phrasing and contractions when it fits");
1068
+ for (const line of rewriteStyleModeInstructions(styleMode)) {
1069
+ promptParts.push(line);
1070
+ }
1071
+ if (input.recentTexts && input.recentTexts.length > 0) {
1072
+ promptParts.push("- avoid vocabulary anchors from these recent outputs:");
1073
+ for (const text of input.recentTexts.slice(0, 5)) {
1074
+ promptParts.push(` - ${text.slice(0, 120)}`);
1075
+ }
1076
+ }
1077
+ try {
1078
+ const rewrite = await generateResponse(system, promptParts.join("\n"));
1079
+ const candidate = cleanRewriteOutput(rewrite.content);
1080
+ if (!candidate) return null;
1081
+ return candidate;
1082
+ } catch {
1083
+ return null;
1084
+ }
1085
+ }
1086
+ function loopClampInt(value, min, max) {
1087
+ return Math.max(min, Math.min(max, Math.round(value)));
1088
+ }
1089
+ function parseJsonObjectFromText(text) {
1090
+ const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i)?.[1];
1091
+ const candidates = [fenced, text].filter((v) => Boolean(v)).map((v) => v.trim());
1092
+ for (const candidate of candidates) {
1093
+ try {
1094
+ const parsed = JSON.parse(candidate);
1095
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1096
+ return parsed;
1097
+ }
1098
+ if (Array.isArray(parsed) && parsed.length > 0 && parsed[0] && typeof parsed[0] === "object") {
1099
+ return parsed[0];
1100
+ }
1101
+ } catch {
1102
+ }
1103
+ const obj = candidate.match(/\{[\s\S]*\}/);
1104
+ if (!obj) continue;
1105
+ try {
1106
+ const parsed = JSON.parse(obj[0]);
1107
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
1108
+ return parsed;
1109
+ }
1110
+ } catch {
1111
+ }
1112
+ }
1113
+ return null;
1114
+ }
1115
+ function mapPlannerTool(raw) {
1116
+ const clean = (raw ?? "").trim().toLowerCase();
1117
+ if (!clean) return null;
1118
+ if (clean === "observe_timeline" || clean === "read_timeline" || clean === "timeline") return "observe_timeline";
1119
+ if (clean === "observe_mentions" || clean === "read_mentions" || clean === "mentions") return "observe_mentions";
1120
+ if (clean === "search_tweets" || clean === "search" || clean === "topic_search") return "search_tweets";
1121
+ if (clean === "check_profile" || clean === "check_person" || clean === "people_check" || clean === "profile_check") return "check_profile";
1122
+ if (clean === "reply") return "reply";
1123
+ if (clean === "like") return "like";
1124
+ if (clean === "retweet") return "retweet";
1125
+ if (clean === "follow") return "follow";
1126
+ if (clean === "post" || clean === "tweet") return "post";
1127
+ if (clean === "skip" || clean === "noop" || clean === "no_action") return "skip";
1128
+ return null;
1129
+ }
1130
+ function parsePlannerDecision(text) {
1131
+ const parsed = parseJsonObjectFromText(text);
1132
+ if (!parsed) return null;
1133
+ const rawTool = typeof parsed.tool === "string" ? parsed.tool : typeof parsed.action === "string" ? parsed.action : void 0;
1134
+ const tool = mapPlannerTool(rawTool);
1135
+ if (!tool) return null;
1136
+ const rawArgs = parsed.args;
1137
+ const args = rawArgs && typeof rawArgs === "object" && !Array.isArray(rawArgs) ? { ...rawArgs } : {};
1138
+ for (const key of ["tweetId", "content", "handle", "query", "count", "reason"]) {
1139
+ if (args[key] !== void 0) continue;
1140
+ if (parsed[key] !== void 0) args[key] = parsed[key];
1141
+ }
1142
+ const reasoning = typeof parsed.reasoning === "string" ? parsed.reasoning.trim() : void 0;
1143
+ return { tool, args, reasoning };
1144
+ }
1145
+ function argString(args, key) {
1146
+ const value = args[key];
1147
+ if (typeof value !== "string") return void 0;
1148
+ const clean = value.trim();
1149
+ return clean.length > 0 ? clean : void 0;
1150
+ }
1151
+ function argCount(args, fallback, min, max) {
1152
+ const raw = args.count;
1153
+ if (typeof raw === "number" && Number.isFinite(raw)) {
1154
+ return loopClampInt(raw, min, max);
1155
+ }
1156
+ if (typeof raw === "string" && raw.trim().length > 0) {
1157
+ const parsed = Number(raw);
1158
+ if (Number.isFinite(parsed)) return loopClampInt(parsed, min, max);
1159
+ }
1160
+ return loopClampInt(fallback, min, max);
1161
+ }
1162
+ function tweetTimestamp(tweet) {
1163
+ const ts = Date.parse(tweet.createdAt);
1164
+ return Number.isNaN(ts) ? 0 : ts;
1165
+ }
1166
+ function mergeTweetList(current, incoming) {
1167
+ const byId = /* @__PURE__ */ new Map();
1168
+ for (const tweet of current) byId.set(tweet.id, tweet);
1169
+ let added = 0;
1170
+ for (const tweet of incoming) {
1171
+ if (!tweet.id) continue;
1172
+ if (!byId.has(tweet.id)) added += 1;
1173
+ byId.set(tweet.id, tweet);
1174
+ }
1175
+ const tweets = [...byId.values()].sort((a, b) => tweetTimestamp(b) - tweetTimestamp(a));
1176
+ return { tweets, added };
1177
+ }
1178
+ function mergeTopicResult(current, query, incoming) {
1179
+ const idx = current.findIndex((item) => item.query.toLowerCase() === query.toLowerCase());
1180
+ if (idx < 0) {
1181
+ const merged2 = mergeTweetList([], incoming);
1182
+ return {
1183
+ next: [...current, { query, tweets: merged2.tweets }],
1184
+ added: merged2.added
1185
+ };
1186
+ }
1187
+ const merged = mergeTweetList(current[idx].tweets, incoming);
1188
+ const next = [...current];
1189
+ next[idx] = { ...next[idx], tweets: merged.tweets };
1190
+ return { next, added: merged.added };
1191
+ }
1192
+ function mergePeopleActivity(current, incoming) {
1193
+ const handle = normalizeHandle2(incoming.handle);
1194
+ const idx = current.findIndex((item) => normalizeHandle2(item.handle) === handle);
1195
+ if (idx < 0) {
1196
+ const merged2 = mergeTweetList([], incoming.tweets);
1197
+ return {
1198
+ next: [...current, { ...incoming, handle, tweets: merged2.tweets }],
1199
+ added: merged2.added
1200
+ };
1201
+ }
1202
+ const merged = mergeTweetList(current[idx].tweets, incoming.tweets);
1203
+ const next = [...current];
1204
+ next[idx] = {
1205
+ ...next[idx],
1206
+ userId: incoming.userId || next[idx].userId,
1207
+ reason: incoming.reason || next[idx].reason,
1208
+ tweets: merged.tweets
1209
+ };
1210
+ return { next, added: merged.added };
1211
+ }
1212
+ function collectObserved(state) {
1213
+ const tweets = [
1214
+ ...state.timeline,
1215
+ ...state.mentions,
1216
+ ...state.topicSearchResults.flatMap((item) => item.tweets),
1217
+ ...state.peopleActivity.flatMap((item) => item.tweets)
1218
+ ];
1219
+ const byId = /* @__PURE__ */ new Map();
1220
+ for (const tweet of tweets) {
1221
+ if (!tweet.id || byId.has(tweet.id)) continue;
1222
+ byId.set(tweet.id, tweet);
1223
+ }
1224
+ return {
1225
+ ids: [...byId.keys()],
1226
+ tweets: [...byId.values()],
1227
+ byId
1228
+ };
1229
+ }
1230
+ function buildPlannerCandidates(state) {
1231
+ const ordered = [
1232
+ ...state.mentions,
1233
+ ...state.timeline,
1234
+ ...state.topicSearchResults.flatMap((item) => item.tweets),
1235
+ ...state.peopleActivity.flatMap((item) => item.tweets)
1236
+ ];
1237
+ const byId = /* @__PURE__ */ new Map();
1238
+ for (const tweet of ordered) {
1239
+ if (!tweet.id) continue;
1240
+ if (!byId.has(tweet.id)) byId.set(tweet.id, tweet);
1241
+ }
1242
+ return [...byId.values()].slice(0, 24);
1243
+ }
1244
+ function buildToolLoopPrompt(input) {
1245
+ const lines = [];
1246
+ lines.push("Choose exactly ONE tool call in strict JSON.");
1247
+ lines.push("You are the autonomous planner. No fixed research sequence is required.");
1248
+ lines.push("Act based on persona + live context, not templates.");
1249
+ lines.push("");
1250
+ lines.push(`Heartbeat: #${input.heartbeatCount}`);
1251
+ lines.push(`Agent: ${input.identity.name} (@${input.identity.handle})`);
1252
+ lines.push(`Bio: ${input.identity.bio}`);
1253
+ lines.push(`Tone: ${input.identity.tone}`);
1254
+ lines.push(`Goals: ${input.identity.goals.join(" | ") || "none"}`);
1255
+ lines.push(`Boundaries: ${input.identity.boundaries.join(" | ") || "none"}`);
1256
+ lines.push("");
1257
+ if (input.constraintLines.length > 0) {
1258
+ lines.push("Hard persona constraints:");
1259
+ for (const line of input.constraintLines) lines.push(line);
1260
+ lines.push("");
1261
+ }
1262
+ if (input.recentMemory.length > 0) {
1263
+ lines.push("Recent memory:");
1264
+ for (const row of input.recentMemory.slice(0, 8)) {
1265
+ lines.push(`- ${row}`);
1266
+ }
1267
+ lines.push("");
1268
+ }
1269
+ lines.push(`Executed actions this heartbeat: ${input.actions.length}`);
1270
+ lines.push(
1271
+ `Current context counts: timeline=${input.state.timeline.length}, mentions=${input.state.mentions.length}, topicTweets=${input.state.topicSearchResults.reduce((sum, item) => sum + item.tweets.length, 0)}, peopleTweets=${input.state.peopleActivity.reduce((sum, item) => sum + item.tweets.length, 0)}`
1272
+ );
1273
+ lines.push("");
1274
+ if (input.toolHistory.length > 0) {
1275
+ lines.push("Recent tool steps:");
1276
+ for (const row of input.toolHistory.slice(-6)) {
1277
+ lines.push(`- Step ${row.step}: ${row.tool} -> ${row.summary}`);
1278
+ }
1279
+ lines.push("");
1280
+ }
1281
+ if (input.policyFeedback.length > 0) {
1282
+ lines.push("Recent policy feedback:");
1283
+ for (const note of input.policyFeedback.slice(-8)) {
1284
+ lines.push(`- ${note}`);
1285
+ }
1286
+ lines.push("");
1287
+ }
1288
+ if (input.candidates.length > 0) {
1289
+ lines.push(`Top candidate tweets (${input.candidates.length}):`);
1290
+ for (const tweet of input.candidates.slice(0, 16)) {
1291
+ lines.push(
1292
+ `- [${tweet.id}] @${tweet.authorHandle}: ${tweet.text.slice(0, 190)} (${tweet.likeCount ?? 0} likes, ${tweet.replyCount ?? 0} replies)`
1293
+ );
1294
+ }
1295
+ lines.push("");
1296
+ } else {
1297
+ lines.push("No candidate tweets currently in context.");
1298
+ lines.push("");
1299
+ }
1300
+ lines.push("Tool options:");
1301
+ lines.push('- observe_timeline: { "count": 8-30 }');
1302
+ lines.push('- observe_mentions: { "count": 8-30 }');
1303
+ lines.push('- search_tweets: { "query": "x api search query", "count": 3-20 }');
1304
+ lines.push('- check_profile: { "handle": "@username", "count": 3-12 }');
1305
+ lines.push('- reply: { "tweetId": "id", "content": "reply under 220 chars" }');
1306
+ lines.push('- like: { "tweetId": "id" }');
1307
+ lines.push('- retweet: { "tweetId": "id" }');
1308
+ lines.push('- follow: { "handle": "@username" }');
1309
+ lines.push('- post: { "content": "post under 220 chars" }');
1310
+ lines.push('- skip: { "reason": "short reason" }');
1311
+ lines.push("");
1312
+ lines.push("Output JSON object only:");
1313
+ lines.push('{ "tool": "reply", "args": { "tweetId": "...", "content": "..." }, "reasoning": "short why" }');
1314
+ lines.push("");
1315
+ lines.push("Planning rules:");
1316
+ lines.push("- if timeline/mentions already have viable tweets, prefer acting over more research");
1317
+ lines.push("- use search/profile checks only when you have a clear reason");
1318
+ lines.push("- avoid repeating the same query/target unless context changed");
1319
+ lines.push("");
1320
+ lines.push("Writing constraints:");
1321
+ lines.push("- sound human and specific, no manifesto language");
1322
+ lines.push("- avoid 'the real question' / 'the deeper question' phrasing");
1323
+ lines.push("- no repetitive anchor terms from recent outputs");
1324
+ lines.push("- match the persona tone strictly");
1325
+ return lines.join("\n");
1326
+ }
1327
+ function fallbackPlannerDecision(step, observedCount) {
1328
+ if (observedCount === 0) {
1329
+ return {
1330
+ tool: step % 2 === 0 ? "observe_timeline" : "observe_mentions",
1331
+ args: { count: 20 },
1332
+ reasoning: "fallback context bootstrap"
1333
+ };
1334
+ }
1335
+ return {
1336
+ tool: "skip",
1337
+ args: { reason: "planner parse fallback; preserve current context" },
1338
+ reasoning: "fallback no-op"
1339
+ };
1340
+ }
1341
+ function decisionToAgentAction(decision) {
1342
+ if (decision.tool === "reply") {
1343
+ const tweetId = argString(decision.args, "tweetId");
1344
+ const content = argString(decision.args, "content");
1345
+ if (!tweetId || !content) return null;
1346
+ return { action: "reply", tweetId, content, reasoning: decision.reasoning };
1347
+ }
1348
+ if (decision.tool === "like") {
1349
+ const tweetId = argString(decision.args, "tweetId");
1350
+ if (!tweetId) return null;
1351
+ return { action: "like", tweetId, reasoning: decision.reasoning };
1352
+ }
1353
+ if (decision.tool === "retweet") {
1354
+ const tweetId = argString(decision.args, "tweetId");
1355
+ if (!tweetId) return null;
1356
+ return { action: "retweet", tweetId, reasoning: decision.reasoning };
1357
+ }
1358
+ if (decision.tool === "follow") {
1359
+ const handle = argString(decision.args, "handle");
1360
+ if (!handle) return null;
1361
+ return { action: "follow", handle, targetHandle: handle, reasoning: decision.reasoning };
1362
+ }
1363
+ if (decision.tool === "post") {
1364
+ const content = argString(decision.args, "content");
1365
+ if (!content) return null;
1366
+ return { action: "post", content, reasoning: decision.reasoning };
1367
+ }
1368
+ if (decision.tool === "skip") {
1369
+ return { action: "skip", reason: argString(decision.args, "reason"), reasoning: decision.reasoning };
1370
+ }
1371
+ return null;
1372
+ }
1373
+ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1374
+ const client = await getXClient();
1375
+ const identity = loadIdentity();
1376
+ const constraints = getPersonaConstraints();
1377
+ const constraintLines = buildPersonaConstraintLines(constraints);
1378
+ if (constraintLines.length > 0) {
1379
+ logger.info(`Persona constraints active: ${constraintLines.join(" | ")}`);
1380
+ }
1381
+ logger.info(`Autonomy runtime v2 active: planner-primary, persona+memory driven.`);
1382
+ try {
1383
+ const delayed = await collectDelayedOutcomes(client);
1384
+ if (delayed.processed > 0) {
1385
+ logger.info(
1386
+ `Delayed outcomes: processed=${delayed.processed}, resolved=${delayed.resolved}, rewardDelta=${delayed.rewardAdded.toFixed(2)}`
1387
+ );
1388
+ }
1389
+ } catch (error) {
1390
+ logger.warn(`Delayed outcome collection failed: ${error.message}`);
1391
+ }
1392
+ const selfHandle = identity.handle.replace(/^@/, "").toLowerCase();
1393
+ const selfUserId = selfUserIdFromCredentials();
1394
+ const isSelfTweet = (tweet) => tweet.authorHandle.replace(/^@/, "").toLowerCase() === selfHandle || selfUserId !== null && tweet.authorId === selfUserId;
1395
+ const state = {
1396
+ timeline: [],
1397
+ mentions: [],
1398
+ topicSearchResults: [],
1399
+ peopleActivity: []
1400
+ };
1401
+ try {
1402
+ const timeline = (await client.getTimeline({ count: 30 })).filter((tweet) => !isSelfTweet(tweet));
1403
+ state.timeline = mergeTweetList(state.timeline, timeline).tweets;
1404
+ } catch (error) {
1405
+ logger.warn(`Seed timeline read failed: ${error.message}`);
1406
+ }
1407
+ try {
1408
+ const mentions = (await client.getMentions({ count: 30 })).filter((tweet) => !isSelfTweet(tweet));
1409
+ state.mentions = mergeTweetList(state.mentions, mentions).tweets;
1410
+ } catch (error) {
1411
+ logger.warn(`Seed mentions read failed: ${error.message}`);
1412
+ }
1413
+ logger.info(
1414
+ `Autonomy context: timeline=${state.timeline.length}, mentions=${state.mentions.length}, topicTweets=${state.topicSearchResults.reduce((sum, item) => sum + item.tweets.length, 0)}, peopleTweets=${state.peopleActivity.reduce((sum, item) => sum + item.tweets.length, 0)}`
1415
+ );
1416
+ const actions = [];
1417
+ const results = [];
1418
+ const toolHistory = [];
1419
+ const policyFeedback = [
1420
+ `Persona: ${identity.name} (@${identity.handle})`,
1421
+ `Tone: ${identity.tone}`,
1422
+ `Goals: ${(identity.goals ?? []).slice(0, 4).join(" | ") || "none"}`
1423
+ ];
1424
+ const systemPrompt = buildSystemPrompt();
1425
+ const toolSystemPrompt = [
1426
+ systemPrompt,
1427
+ "TOOL LOOP MODE: You must choose exactly one tool call each step using strict JSON.",
1428
+ "Planner-first mode: there is no pre-defined research flow.",
1429
+ "Everything must be persona-driven, memory-aware, and context-grounded.",
1430
+ "Do not fall into repetitive searches or repetitive targets."
1431
+ ].join("\n\n");
1432
+ const activeIntents = listIntents();
1433
+ let staleSteps = 0;
1434
+ let attemptedActions = 0;
1435
+ const maxToolSteps = Math.max(12, maxActions * 6);
1436
+ getRecentInteractions(20);
1437
+ for (let step = 0; step < maxToolSteps && actions.length < maxActions; step += 1) {
1438
+ const candidates = buildPlannerCandidates(state);
1439
+ const recentMemory = getRecentInteractions(24).slice(0, 12).map((entry) => {
1440
+ const text = (entry.content ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
1441
+ const target = entry.targetHandle ? ` @${entry.targetHandle}` : "";
1442
+ return `${entry.type}${target}${text ? `: ${text}` : ""}`;
1443
+ });
1444
+ const prompt = buildToolLoopPrompt({
1445
+ identity: {
1446
+ name: identity.name,
1447
+ handle: identity.handle,
1448
+ bio: identity.bio,
1449
+ tone: identity.tone,
1450
+ goals: identity.goals ?? [],
1451
+ boundaries: identity.boundaries ?? []
1452
+ },
1453
+ constraintLines,
1454
+ candidates,
1455
+ state,
1456
+ actions,
1457
+ policyFeedback,
1458
+ toolHistory,
1459
+ recentMemory,
1460
+ heartbeatCount
1461
+ });
1462
+ let decision = fallbackPlannerDecision(step, candidates.length);
1463
+ try {
1464
+ const response = await generateResponse(toolSystemPrompt, prompt);
1465
+ const parsed = parsePlannerDecision(response.content);
1466
+ if (parsed) {
1467
+ decision = parsed;
1468
+ } else {
1469
+ policyFeedback.push("Planner returned invalid JSON decision. Using fallback tool.");
1470
+ }
1471
+ } catch (error) {
1472
+ policyFeedback.push(`Planner call failed: ${error.message}`);
1473
+ }
1474
+ logger.info(`Tool loop step ${step + 1}: ${decision.tool}`);
1475
+ let stepProgress = false;
1476
+ if (decision.tool === "observe_timeline") {
1477
+ const count = argCount(decision.args, 16, 8, 30);
1478
+ try {
1479
+ const incoming = (await client.getTimeline({ count })).filter((tweet) => !isSelfTweet(tweet));
1480
+ const merged = mergeTweetList(state.timeline, incoming);
1481
+ state.timeline = merged.tweets;
1482
+ stepProgress = merged.added > 0;
1483
+ toolHistory.push({
1484
+ step: step + 1,
1485
+ tool: decision.tool,
1486
+ summary: `fetched ${incoming.length}, added ${merged.added}`
1487
+ });
1488
+ } catch (error) {
1489
+ policyFeedback.push(`observe_timeline failed: ${error.message}`);
1490
+ }
1491
+ if (!stepProgress) policyFeedback.push("observe_timeline found no new tweets.");
1492
+ continue;
1493
+ }
1494
+ if (decision.tool === "observe_mentions") {
1495
+ const count = argCount(decision.args, 16, 8, 30);
1496
+ try {
1497
+ const incoming = (await client.getMentions({ count })).filter((tweet) => !isSelfTweet(tweet));
1498
+ const merged = mergeTweetList(state.mentions, incoming);
1499
+ state.mentions = merged.tweets;
1500
+ stepProgress = merged.added > 0;
1501
+ toolHistory.push({
1502
+ step: step + 1,
1503
+ tool: decision.tool,
1504
+ summary: `fetched ${incoming.length}, added ${merged.added}`
1505
+ });
1506
+ } catch (error) {
1507
+ policyFeedback.push(`observe_mentions failed: ${error.message}`);
1508
+ }
1509
+ if (!stepProgress) policyFeedback.push("observe_mentions found no new tweets.");
1510
+ continue;
1511
+ }
1512
+ if (decision.tool === "search_tweets") {
1513
+ const rawQuery = argString(decision.args, "query");
1514
+ if (!rawQuery) {
1515
+ policyFeedback.push("search_tweets rejected: missing query.");
1516
+ continue;
1517
+ }
1518
+ const query = canonicalizePlannerQuery(rawQuery);
1519
+ if (query !== rawQuery) {
1520
+ policyFeedback.push(`search_tweets normalized handle query: "${rawQuery}" -> "${query}"`);
1521
+ }
1522
+ const count = argCount(decision.args, 10, 3, 20);
1523
+ try {
1524
+ const incoming = (await client.searchTweets(query, { count })).filter((tweet) => !isSelfTweet(tweet));
1525
+ const merged = mergeTopicResult(state.topicSearchResults, query, incoming);
1526
+ state.topicSearchResults = merged.next;
1527
+ stepProgress = merged.added > 0;
1528
+ toolHistory.push({
1529
+ step: step + 1,
1530
+ tool: decision.tool,
1531
+ summary: `"${query}" fetched ${incoming.length}, added ${merged.added}`
1532
+ });
1533
+ logger.info(`Tool search "${query}": found ${incoming.length} tweets`);
1534
+ } catch (error) {
1535
+ policyFeedback.push(`search_tweets failed for "${query}": ${error.message}`);
1536
+ }
1537
+ if (!stepProgress) policyFeedback.push(`search_tweets "${query}" found no new tweets.`);
1538
+ continue;
1539
+ }
1540
+ if (decision.tool === "check_profile") {
1541
+ const handleInput = argString(decision.args, "handle");
1542
+ const handle = handleInput ? canonicalPlannerHandle(handleInput) : "";
1543
+ if (!handle) {
1544
+ policyFeedback.push("check_profile rejected: missing handle.");
1545
+ continue;
1546
+ }
1547
+ const count = argCount(decision.args, 6, 3, 12);
1548
+ try {
1549
+ const profile = await client.getProfile(handle);
1550
+ const tweets = (await client.getUserTweets(profile.id, { count })).filter((tweet) => !isSelfTweet(tweet));
1551
+ const merged = mergePeopleActivity(state.peopleActivity, {
1552
+ handle,
1553
+ userId: profile.id,
1554
+ reason: "tool-loop check",
1555
+ tweets
1556
+ });
1557
+ state.peopleActivity = merged.next;
1558
+ stepProgress = merged.added > 0;
1559
+ toolHistory.push({
1560
+ step: step + 1,
1561
+ tool: decision.tool,
1562
+ summary: `@${handle} fetched ${tweets.length}, added ${merged.added}`
1563
+ });
1564
+ logger.info(`People check @${handle}: ${tweets.length} recent tweets`);
1565
+ } catch (error) {
1566
+ policyFeedback.push(`check_profile failed for @${handle}: ${error.message}`);
1567
+ }
1568
+ if (!stepProgress) policyFeedback.push(`check_profile @${handle} found no new tweets.`);
1569
+ continue;
1570
+ }
1571
+ const observed = collectObserved(state);
1572
+ let candidateAction = decisionToAgentAction(decision);
1573
+ if (!candidateAction) {
1574
+ policyFeedback.push(`Tool decision ${decision.tool} rejected: invalid arguments.`);
1575
+ staleSteps += 1;
1576
+ continue;
1577
+ }
1578
+ if (candidateAction.tweetId && observed.byId.has(candidateAction.tweetId)) {
1579
+ const author = normalizeHandle2(observed.byId.get(candidateAction.tweetId)?.authorHandle);
1580
+ if (author && !candidateAction.targetHandle) {
1581
+ candidateAction.targetHandle = `@${author}`;
1582
+ }
1583
+ }
1584
+ if (candidateAction.action === "skip") {
1585
+ const reason = candidateAction.reason ?? candidateAction.reasoning ?? "planner skip";
1586
+ policyFeedback.push(`Planner chose skip: ${reason}`);
1587
+ toolHistory.push({
1588
+ step: step + 1,
1589
+ tool: decision.tool,
1590
+ summary: reason
1591
+ });
1592
+ staleSteps += 1;
1593
+ if (staleSteps >= 4) break;
1594
+ continue;
1595
+ }
1596
+ attemptedActions += 1;
1597
+ let policy = evaluateActionPolicy({
1598
+ action: candidateAction,
1599
+ step: attemptedActions,
1600
+ timeline: state.timeline,
1601
+ mentions: state.mentions,
1602
+ executedActions: actions,
1603
+ observedTweetIds: observed.ids,
1604
+ observedTweets: observed.tweets,
1605
+ selfHandle,
1606
+ selfUserId
1607
+ });
1608
+ if (!policy.allowed && shouldAttemptStyleRewrite(candidateAction, policy.reason ?? "")) {
1609
+ const recentTexts = getRecentInteractions(10).filter((entry) => entry.type === "post" || entry.type === "reply").map((entry) => entry.content ?? "").filter(Boolean);
1610
+ const rewritten = await rewriteDraftForHumanVoice({
1611
+ action: candidateAction,
1612
+ reason: policy.reason ?? "policy rejected",
1613
+ targetTweetText: candidateAction.tweetId ? observed.byId.get(candidateAction.tweetId)?.text : void 0,
1614
+ recentTexts
1615
+ });
1616
+ if (rewritten && rewritten !== candidateAction.content) {
1617
+ candidateAction = {
1618
+ ...candidateAction,
1619
+ content: rewritten,
1620
+ reasoning: `${candidateAction.reasoning ?? "rewritten"} | style rewrite`
1621
+ };
1622
+ policy = evaluateActionPolicy({
1623
+ action: candidateAction,
1624
+ step: attemptedActions,
1625
+ timeline: state.timeline,
1626
+ mentions: state.mentions,
1627
+ executedActions: actions,
1628
+ observedTweetIds: observed.ids,
1629
+ observedTweets: observed.tweets,
1630
+ selfHandle,
1631
+ selfUserId
1632
+ });
1633
+ }
1634
+ }
1635
+ if (!policy.allowed) {
1636
+ const reason = policy.reason ?? "Policy rejected action";
1637
+ policyFeedback.push(`Policy advisory (not blocking): ${reason}`);
1638
+ logger.info(`Policy advisory for ${candidateAction.action}: ${reason}`);
1639
+ }
1640
+ const result = await executeAction(candidateAction);
1641
+ actions.push(candidateAction);
1642
+ results.push(result);
1643
+ staleSteps = 0;
1644
+ stepProgress = true;
1645
+ toolHistory.push({
1646
+ step: step + 1,
1647
+ tool: decision.tool,
1648
+ summary: `${candidateAction.action}${result.success ? " success" : " failed"}`
1649
+ });
1650
+ if (activeIntents.length > 0) {
1651
+ const touchedHandle = normalizeHandle2(
1652
+ candidateAction.targetHandle ?? candidateAction.handle ?? (candidateAction.tweetId ? observed.byId.get(candidateAction.tweetId)?.authorHandle : void 0)
1653
+ );
1654
+ const loweredContent = (candidateAction.content ?? "").toLowerCase();
1655
+ for (const intent of activeIntents) {
1656
+ const handleMatch = touchedHandle.length > 0 && intent.targetHandles.some((handle) => normalizeHandle2(handle) === touchedHandle);
1657
+ const topicMatch = loweredContent.length > 0 && intent.focusTopics.some((topic) => loweredContent.includes(topic.toLowerCase()));
1658
+ if (!handleMatch && !topicMatch) continue;
1659
+ recordIntentExecution(intent.id, {
1660
+ success: result.success,
1661
+ note: `${candidateAction.action}${touchedHandle ? ` @${touchedHandle}` : ""}`
1662
+ });
1663
+ }
1664
+ }
1665
+ if (!result.success) {
1666
+ const err = result.error ?? "";
1667
+ if (candidateAction.action === "reply" && /duplicate content/i.test(err)) {
1668
+ const reason = "Reply failed with duplicate-content error. Planner should choose a different wording/target.";
1669
+ policyFeedback.push(reason);
1670
+ logger.info(`Policy adjustment: ${reason}`);
1671
+ }
1672
+ if (candidateAction.action === "post" && /duplicate content/i.test(err)) {
1673
+ const reason = "Post failed with duplicate-content error. Change framing and try a different angle.";
1674
+ policyFeedback.push(reason);
1675
+ logger.info(`Policy adjustment: ${reason}`);
1676
+ }
1677
+ }
1678
+ if (!stepProgress) {
1679
+ staleSteps += 1;
1680
+ }
1681
+ if (staleSteps >= 5) {
1682
+ logger.info("Tool loop stopped after repeated no-progress steps.");
1683
+ break;
1684
+ }
1685
+ }
1686
+ recordBanditActionResults(actions, results);
1687
+ return {
1688
+ timeline: state.timeline,
1689
+ mentions: state.mentions,
1690
+ actions,
1691
+ results,
1692
+ policyFeedback
1693
+ };
1694
+ }
1695
+
1696
+ export {
1697
+ runAutonomyCycle
1698
+ };
1699
+ //# sourceMappingURL=chunk-TTDQZI5W.js.map