spora 0.7.5 → 0.7.7

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.
@@ -6,8 +6,12 @@ import {
6
6
  } from "./chunk-ZLSDFYBR.js";
7
7
  import {
8
8
  buildOpportunityPortfolioMessage,
9
- buildSystemPrompt
10
- } from "./chunk-A2XTKC7B.js";
9
+ buildPersonaConstraintLines,
10
+ buildSystemPrompt,
11
+ compilePersonaActionProfile,
12
+ getPersonaConstraints,
13
+ personaConstraintHandles
14
+ } from "./chunk-JIMONWKO.js";
11
15
  import {
12
16
  listIntents,
13
17
  loadStrategy,
@@ -510,6 +514,12 @@ function isWritingAction(action) {
510
514
  function normalizeHandle(handle) {
511
515
  return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
512
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
+ }
513
523
  function executedWrittenContent(executedActions) {
514
524
  return executedActions.filter((a) => isWritingAction(a) && typeof a.content === "string").map((a) => a.content?.trim() ?? "").filter((content) => content.length > 0);
515
525
  }
@@ -583,6 +593,44 @@ function evaluateActionPolicy(context) {
583
593
  const selfId = (selfUserId ?? "").trim();
584
594
  const knownTweets = observedTweets ?? [...timeline, ...mentions];
585
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
+ }
586
634
  if (isDuplicateTarget(action, executedActions)) {
587
635
  return { allowed: false, reason: `Action ${action.action} already executed for tweet ${action.tweetId} this heartbeat.` };
588
636
  }
@@ -736,8 +784,9 @@ function evaluateActionPolicy(context) {
736
784
  }
737
785
  }
738
786
  if (action.action === "reply" && action.tweetId) {
787
+ const inStrictTargetMode = strictReplyHandles.size > 0 || strictInteractHandles.size > 0;
739
788
  const repliesThisHeartbeat = executedActions.filter((a) => a.action === "reply").length;
740
- if (repliesThisHeartbeat >= 2) {
789
+ if (repliesThisHeartbeat >= 2 && !inStrictTargetMode) {
741
790
  return {
742
791
  allowed: false,
743
792
  reason: "Reply cap reached for this heartbeat. Use another tool action."
@@ -748,7 +797,7 @@ function evaluateActionPolicy(context) {
748
797
  const repliedAuthors = new Set(
749
798
  executedActions.filter((a) => a.action === "reply" && a.tweetId).map((a) => normalizeHandle(tweetById.get(a.tweetId)?.authorHandle)).filter(Boolean)
750
799
  );
751
- if (repliedAuthors.has(currentAuthor)) {
800
+ if (repliedAuthors.has(currentAuthor) && !inStrictTargetMode) {
752
801
  return {
753
802
  allowed: false,
754
803
  reason: `Already replied to @${currentAuthor} this heartbeat. Diversify interactions.`
@@ -973,6 +1022,9 @@ function shuffle(items) {
973
1022
  }
974
1023
  return arr;
975
1024
  }
1025
+ function clampInt(value, min, max) {
1026
+ return Math.max(min, Math.min(max, Math.round(value)));
1027
+ }
976
1028
  function buildTopicQueryVariants(topic) {
977
1029
  const trimmed = topic.trim();
978
1030
  if (!trimmed) return [];
@@ -1194,21 +1246,38 @@ async function runResearchPhase(client, heartbeatCount) {
1194
1246
  async function runTopicSearch(client, heartbeatCount, seedTweets) {
1195
1247
  try {
1196
1248
  const identity = loadIdentity();
1249
+ const constraints = getPersonaConstraints(identity);
1250
+ const strictHandles = personaConstraintHandles(constraints).filter(Boolean);
1197
1251
  const strategy = loadStrategy();
1252
+ const personaProfile = compilePersonaActionProfile({ identity, strategy, constraints });
1198
1253
  const selfHandle = canonicalAgentHandle(identity.handle);
1254
+ const topicSearchBias = personaProfile.sourceBias.topic_search;
1255
+ const peopleWatchBias = personaProfile.sourceBias.people_watch;
1199
1256
  const allTopics = [
1257
+ ...personaProfile.priorityTopics,
1200
1258
  ...identity.topics ?? [],
1201
1259
  ...strategy.currentFocus
1202
1260
  ].flatMap(splitTopicSignal).filter(Boolean);
1203
- const discovered = discoverQueriesFromTweets(seedTweets, 3);
1261
+ const discoveredBudget = clampInt(1 + Math.max(0, topicSearchBias), 1, 3);
1262
+ const discovered = discoverQueriesFromTweets(seedTweets, discoveredBudget);
1204
1263
  const targetHandles = [.../* @__PURE__ */ new Set([
1205
1264
  ...strategy.peopleToEngage.filter((person) => person.priority === "high" || person.priority === "medium").map((person) => person.handle.replace(/^@/, "").toLowerCase()),
1206
1265
  ...(identity.heroes ?? []).map((handle) => handle.replace(/^@/, "").toLowerCase())
1207
1266
  ])].filter((handle) => handle && handle !== selfHandle);
1208
- const personQueries = buildTargetedPersonQueries(targetHandles, heartbeatCount, 2);
1209
- const topicQueries = allTopics.length > 0 ? pickTopicQueries(allTopics, Math.min(3, Math.max(2, allTopics.length))) : [];
1210
- const discoveredQueries = discovered.length > 0 ? pickDirectQueries(discovered, Math.min(2, discovered.length)) : [];
1211
- const topicsToSearch = [.../* @__PURE__ */ new Set([...topicQueries, ...discoveredQueries, ...personQueries])];
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])];
1212
1281
  if (topicsToSearch.length === 0) return [];
1213
1282
  const results = [];
1214
1283
  for (const topicQuery of topicsToSearch) {
@@ -1232,10 +1301,35 @@ async function runPeopleMonitoring(client, heartbeatCount, seedTweets) {
1232
1301
  try {
1233
1302
  const strategy = loadStrategy();
1234
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);
1235
1307
  const selfHandle = identity.handle.replace(/^@/, "").toLowerCase();
1236
1308
  const relationships = loadRelationships();
1237
1309
  const networkHandles = listAgentNetworkHandles(20);
1238
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
+ }
1239
1333
  const people = [];
1240
1334
  const seen = /* @__PURE__ */ new Set();
1241
1335
  const highPriorityHandles = new Set(
@@ -1257,9 +1351,10 @@ async function runPeopleMonitoring(client, heartbeatCount, seedTweets) {
1257
1351
  for (const p of strategy.peopleToEngage.filter((p2) => p2.priority === "high")) {
1258
1352
  addPerson(p.handle, p.reason, true);
1259
1353
  }
1354
+ const sporaPinInterval = personaProfile.sourceBias.people_watch >= 0.6 ? 4 : 8;
1260
1355
  for (const handle of networkHandles) {
1261
1356
  if (handle === "sporaai") {
1262
- const pinSpora = heartbeatCount % 4 === 0;
1357
+ const pinSpora = heartbeatCount % sporaPinInterval === 0;
1263
1358
  addPerson(handle, "core Spora profile", pinSpora);
1264
1359
  } else {
1265
1360
  addPerson(handle, "spora agent network");
@@ -1284,7 +1379,8 @@ async function runPeopleMonitoring(client, heartbeatCount, seedTweets) {
1284
1379
  addPerson(handle, "active voice in current conversations");
1285
1380
  }
1286
1381
  if (people.length === 0) return [];
1287
- const budget = Math.min(3, people.length);
1382
+ const monitoringBudget = clampInt(3 + personaProfile.sourceBias.people_watch, 2, 5);
1383
+ const budget = Math.min(monitoringBudget, people.length);
1288
1384
  const selected = [];
1289
1385
  const selectedHandles = /* @__PURE__ */ new Set();
1290
1386
  const pinned = people.filter((person) => person.pinned);
@@ -1384,7 +1480,9 @@ function unique(values) {
1384
1480
  }
1385
1481
  function buildPersonaContext() {
1386
1482
  const identity = loadIdentity();
1483
+ const constraints = getPersonaConstraints(identity);
1387
1484
  const strategy = loadStrategy();
1485
+ const actionProfile = compilePersonaActionProfile({ identity, strategy, constraints });
1388
1486
  const focusKeywords = unique(
1389
1487
  [
1390
1488
  ...identity.topics,
@@ -1400,11 +1498,29 @@ function buildPersonaContext() {
1400
1498
  const priorityHandles = new Set(
1401
1499
  strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => normalizeHandle3(person.handle)).filter(Boolean)
1402
1500
  );
1501
+ for (const handle of personaConstraintHandles(constraints)) {
1502
+ priorityHandles.add(normalizeHandle3(handle));
1503
+ }
1403
1504
  const heroHandles = new Set(
1404
1505
  (identity.heroes ?? []).map((handle) => normalizeHandle3(handle)).filter(Boolean)
1405
1506
  );
1406
1507
  const networkHandles = new Set(listAgentNetworkHandles(60).map((handle) => normalizeHandle3(handle)).filter(Boolean));
1407
- return { focusKeywords, avoidKeywords, priorityHandles, heroHandles, networkHandles };
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;
1408
1524
  }
1409
1525
  function phraseMatchScore(text, keywords) {
1410
1526
  if (keywords.length === 0) return 0;
@@ -1505,7 +1621,11 @@ function buildActionOpportunities(input) {
1505
1621
  const maxCandidates = input.maxCandidates ?? 28;
1506
1622
  const selfHandle = normalizeHandle3(input.selfHandle);
1507
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)));
1508
1627
  const persona = buildPersonaContext();
1628
+ const actionProfile = persona.actionProfile;
1509
1629
  const recent = getRecentInteractions(300);
1510
1630
  const {
1511
1631
  repliedTweetIds,
@@ -1529,6 +1649,7 @@ function buildActionOpportunities(input) {
1529
1649
  const handle = normalizeHandle3(candidate.tweet.authorHandle);
1530
1650
  if (!candidate.tweet.id || !handle || handle === selfHandle) continue;
1531
1651
  if (selfUserId && candidate.tweet.authorId === selfUserId) continue;
1652
+ if (onlyInteractHandles.size > 0 && !onlyInteractHandles.has(handle)) continue;
1532
1653
  const existing = dedupedByTweetId.get(candidate.tweet.id);
1533
1654
  if (!existing || sourceWeight(candidate.source) > sourceWeight(existing.source)) {
1534
1655
  dedupedByTweetId.set(candidate.tweet.id, candidate);
@@ -1552,27 +1673,33 @@ function buildActionOpportunities(input) {
1552
1673
  persona
1553
1674
  );
1554
1675
  const shortTweet = clip(tweet.text, 170);
1555
- opportunities.push({
1556
- id: nextId(),
1557
- armKey: armKey("reply", source),
1558
- actionType: "reply",
1559
- source,
1560
- score: baseScore + 0.7,
1561
- summary: `Reply to @${handle} from ${source}`,
1562
- authorHandle: handle,
1563
- tweetId: tweet.id,
1564
- requiresContent: true,
1565
- template: { action: "reply", tweetId: tweet.id, source, targetHandle: `@${handle}` },
1566
- context: `@${handle}: "${shortTweet}"`
1567
- });
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
+ }
1568
1695
  const stronglyRelevant = alignment >= 0.35 || source === "mention" || source === "people_watch" || persona.priorityHandles.has(handle);
1569
- if (!likedTweetIds.has(tweet.id) && baseScore >= 1.8 && stronglyRelevant) {
1696
+ if (onlyReplyHandles.size === 0 && !constraints.replyOnlyMode && !isActionStronglySuppressed("like", actionProfile) && matchesPersonaGeneratedProfile("like", source, actionProfile) && !likedTweetIds.has(tweet.id) && likeScore >= 1.8 && stronglyRelevant) {
1570
1697
  opportunities.push({
1571
1698
  id: nextId(),
1572
1699
  armKey: armKey("like", source),
1573
1700
  actionType: "like",
1574
1701
  source,
1575
- score: baseScore + 0.2,
1702
+ score: likeScore,
1576
1703
  summary: `Like @${handle} from ${source}`,
1577
1704
  authorHandle: handle,
1578
1705
  tweetId: tweet.id,
@@ -1581,13 +1708,13 @@ function buildActionOpportunities(input) {
1581
1708
  context: `@${handle}: "${shortTweet}"`
1582
1709
  });
1583
1710
  }
1584
- if (!retweetedTweetIds.has(tweet.id) && baseScore >= 2.6 && alignment >= 0.15) {
1711
+ if (onlyReplyHandles.size === 0 && !constraints.replyOnlyMode && !isActionStronglySuppressed("retweet", actionProfile) && matchesPersonaGeneratedProfile("retweet", source, actionProfile) && !retweetedTweetIds.has(tweet.id) && retweetScore >= 2.6 && alignment >= 0.15) {
1585
1712
  opportunities.push({
1586
1713
  id: nextId(),
1587
1714
  armKey: armKey("retweet", source),
1588
1715
  actionType: "retweet",
1589
1716
  source,
1590
- score: baseScore - 0.1,
1717
+ score: retweetScore,
1591
1718
  summary: `Retweet @${handle} from ${source}`,
1592
1719
  authorHandle: handle,
1593
1720
  tweetId: tweet.id,
@@ -1596,7 +1723,7 @@ function buildActionOpportunities(input) {
1596
1723
  context: `@${handle}: "${shortTweet}"`
1597
1724
  });
1598
1725
  }
1599
- const shouldConsiderFollow = !followedHandles.has(handle) && !followOpportunityByHandle.has(handle) && !isLikelySyntheticHandle2(handle) && baseScore >= 3 && (persona.priorityHandles.has(handle) || persona.heroHandles.has(handle) || source === "mention" || alignment >= 0.8);
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);
1600
1727
  if (shouldConsiderFollow) {
1601
1728
  followOpportunityByHandle.add(handle);
1602
1729
  opportunities.push({
@@ -1604,7 +1731,7 @@ function buildActionOpportunities(input) {
1604
1731
  armKey: armKey("follow", source),
1605
1732
  actionType: "follow",
1606
1733
  source,
1607
- score: baseScore - 0.2,
1734
+ score: followScore,
1608
1735
  summary: `Follow @${handle}`,
1609
1736
  authorHandle: handle,
1610
1737
  requiresContent: false,
@@ -1614,17 +1741,19 @@ function buildActionOpportunities(input) {
1614
1741
  }
1615
1742
  }
1616
1743
  const queryList = research.topicSearchResults.map((r) => r.query).filter(Boolean);
1617
- if (queryList.length > 0) {
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(", ");
1618
1747
  opportunities.push({
1619
1748
  id: nextId(),
1620
1749
  armKey: armKey("post", "synthesis"),
1621
1750
  actionType: "post",
1622
1751
  source: "synthesis",
1623
- score: 2.25,
1752
+ score: synthesisScore,
1624
1753
  summary: "Post an original thought synthesized from active conversations",
1625
1754
  requiresContent: true,
1626
1755
  template: { action: "post", source: "synthesis" },
1627
- context: `Weave a fresh take from active topics: ${queryList.slice(0, 4).join(", ")}`
1756
+ context: `Weave a fresh take from active topics: ${queryList.slice(0, 4).join(", ")}${priorityTopicHint ? `. Persona-priority topics: ${priorityTopicHint}` : ""}`
1628
1757
  });
1629
1758
  }
1630
1759
  opportunities.sort((a, b) => b.score - a.score);
@@ -1697,6 +1826,202 @@ async function planActionPortfolio(input) {
1697
1826
  return actions;
1698
1827
  }
1699
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
+
1700
2025
  // src/runtime/bandit.ts
1701
2026
  import { existsSync as existsSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
1702
2027
  var DEFAULT_EXPLORATION_BUDGET = 0.25;
@@ -1941,7 +2266,7 @@ function selfUserIdFromCredentials() {
1941
2266
  return null;
1942
2267
  }
1943
2268
  }
1944
- function normalizeHandle4(handle) {
2269
+ function normalizeHandle5(handle) {
1945
2270
  return (handle ?? "").replace(/^@/, "").trim().toLowerCase();
1946
2271
  }
1947
2272
  function roughWordCount(text) {
@@ -2011,6 +2336,64 @@ function shouldAttemptStyleRewrite(action, reason) {
2011
2336
  const lower = reason.toLowerCase();
2012
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");
2013
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
+ }
2014
2397
  function cleanRewriteOutput(text) {
2015
2398
  const cleaned = text.replace(/```[\s\S]*?```/g, "").replace(/^["'`]+|["'`]+$/g, "").trim();
2016
2399
  const firstLine = cleaned.split("\n").map((line) => line.trim()).filter(Boolean)[0] ?? cleaned;
@@ -2062,6 +2445,19 @@ async function rewriteDraftForHumanVoice(input) {
2062
2445
  }
2063
2446
  async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2064
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
+ );
2065
2461
  try {
2066
2462
  const delayed = await collectDelayedOutcomes(client);
2067
2463
  if (delayed.processed > 0) {
@@ -2072,7 +2468,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2072
2468
  } catch (error) {
2073
2469
  logger.warn(`Delayed outcome collection failed: ${error.message}`);
2074
2470
  }
2075
- const selfHandle = loadIdentity().handle.replace(/^@/, "").toLowerCase();
2471
+ const selfHandle = identity.handle.replace(/^@/, "").toLowerCase();
2076
2472
  const selfUserId = selfUserIdFromCredentials();
2077
2473
  const isSelfTweet = (tweet) => tweet.authorHandle.replace(/^@/, "").toLowerCase() === selfHandle || selfUserId !== null && tweet.authorId === selfUserId;
2078
2474
  const research = await runResearchPhase(client, heartbeatCount);
@@ -2101,15 +2497,39 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2101
2497
  ...filteredTopicSearchResults.flatMap((result) => result.tweets),
2102
2498
  ...filteredPeopleActivity.flatMap((person) => person.tweets)
2103
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
+ );
2104
2510
  logger.info(
2105
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)}`
2106
2512
  );
2107
2513
  const systemPrompt = buildSystemPrompt();
2108
2514
  const actions = [];
2109
2515
  const results = [];
2110
- const policyFeedback = [];
2516
+ const policyFeedback = [
2517
+ `Heartbeat mission: ${mission.objective}`,
2518
+ `Mission mode: ${mission.mode}`
2519
+ ];
2111
2520
  const blockedTweetIds = new Set(recentReplyTargetIds);
2112
- const disallowedActions = /* @__PURE__ */ new Set();
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));
2113
2533
  let replyRejectionCount = 0;
2114
2534
  const opportunities = buildActionOpportunities({
2115
2535
  research: {
@@ -2123,27 +2543,28 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2123
2543
  selfUserId,
2124
2544
  maxCandidates: 30
2125
2545
  });
2126
- const planningPool = selectBanditOpportunityPool(opportunities, 24);
2546
+ const personaSeedPool = buildPersonaGeneratedPool(opportunities, 30, personaProfile);
2547
+ const banditPool = selectBanditOpportunityPool(personaSeedPool, 24);
2548
+ const planningPool = buildPersonaGeneratedPool(banditPool, 24, personaProfile);
2127
2549
  const byType = planningPool.reduce((acc, opportunity) => {
2128
2550
  acc[opportunity.actionType] = (acc[opportunity.actionType] ?? 0) + 1;
2129
2551
  return acc;
2130
2552
  }, {});
2131
2553
  logger.info(`Opportunities selected: total=${planningPool.length}, byType=${JSON.stringify(byType)}`);
2132
- const strategy = loadStrategy();
2133
2554
  const activeIntents = listIntents();
2134
2555
  const intentTargetHandles = new Set(
2135
- activeIntents.flatMap((intent) => intent.targetHandles.map((handle) => normalizeHandle4(handle)))
2556
+ activeIntents.flatMap((intent) => intent.targetHandles.map((handle) => normalizeHandle5(handle)))
2136
2557
  );
2137
2558
  const priorityHandles = /* @__PURE__ */ new Set(
2138
2559
  [
2139
- ...strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => normalizeHandle4(person.handle)).filter(Boolean),
2560
+ ...strategy.peopleToEngage.filter((person) => person.priority === "high").map((person) => normalizeHandle5(person.handle)).filter(Boolean),
2140
2561
  ...intentTargetHandles
2141
2562
  ]
2142
2563
  );
2143
2564
  const authorByTweetId = /* @__PURE__ */ new Map();
2144
2565
  for (const opportunity of planningPool) {
2145
2566
  if (opportunity.tweetId && opportunity.authorHandle) {
2146
- authorByTweetId.set(opportunity.tweetId, normalizeHandle4(opportunity.authorHandle));
2567
+ authorByTweetId.set(opportunity.tweetId, normalizeHandle5(opportunity.authorHandle));
2147
2568
  }
2148
2569
  }
2149
2570
  getRecentInteractions(20);
@@ -2151,6 +2572,10 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2151
2572
  return planningPool.filter((opportunity) => {
2152
2573
  if (disallowedActions.has(opportunity.actionType)) return false;
2153
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
+ }
2154
2579
  return true;
2155
2580
  });
2156
2581
  };
@@ -2167,7 +2592,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2167
2592
  policyFeedback,
2168
2593
  executedActions: actions
2169
2594
  });
2170
- if (plannedActions.length > 1 && plannedActions.every((a) => a.action === "reply")) {
2595
+ if (!strictReplyOnly && plannedActions.length > 1 && plannedActions.every((a) => a.action === "reply")) {
2171
2596
  const nonReplyCandidate = candidates.find(
2172
2597
  (opportunity) => opportunity.actionType !== "reply" && !opportunity.requiresContent
2173
2598
  );
@@ -2182,18 +2607,18 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2182
2607
  const authorByTweetId2 = /* @__PURE__ */ new Map();
2183
2608
  for (const opportunity of candidates) {
2184
2609
  if (opportunity.tweetId && opportunity.authorHandle) {
2185
- authorByTweetId2.set(opportunity.tweetId, normalizeHandle4(opportunity.authorHandle));
2610
+ authorByTweetId2.set(opportunity.tweetId, normalizeHandle5(opportunity.authorHandle));
2186
2611
  }
2187
2612
  }
2188
2613
  const touchesPriorityTarget = plannedActions.some((action) => {
2189
- if (action.handle && priorityHandles.has(normalizeHandle4(action.handle))) return true;
2190
- if (action.targetHandle && priorityHandles.has(normalizeHandle4(action.targetHandle))) return true;
2614
+ if (action.handle && priorityHandles.has(normalizeHandle5(action.handle))) return true;
2615
+ if (action.targetHandle && priorityHandles.has(normalizeHandle5(action.targetHandle))) return true;
2191
2616
  if (action.tweetId && priorityHandles.has(authorByTweetId2.get(action.tweetId) ?? "")) return true;
2192
2617
  return false;
2193
2618
  });
2194
2619
  if (!touchesPriorityTarget) {
2195
2620
  const priorityCandidate = candidates.find(
2196
- (opportunity) => Boolean(opportunity.authorHandle) && priorityHandles.has(normalizeHandle4(opportunity.authorHandle)) && !opportunity.requiresContent
2621
+ (opportunity) => Boolean(opportunity.authorHandle) && priorityHandles.has(normalizeHandle5(opportunity.authorHandle)) && (!strictReplyOnly ? !opportunity.requiresContent : opportunity.actionType === "reply")
2197
2622
  );
2198
2623
  if (priorityCandidate) {
2199
2624
  plannedActions = [
@@ -2271,7 +2696,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2271
2696
  }
2272
2697
  if (candidateAction.action === "reply") {
2273
2698
  replyRejectionCount += 1;
2274
- if (replyRejectionCount >= 2) {
2699
+ if (replyRejectionCount >= 2 && !strictReplyOnly) {
2275
2700
  disallowedActions.add("reply");
2276
2701
  const pivot = "Reply opportunities exhausted this heartbeat. Pivot to like/retweet/follow/post.";
2277
2702
  policyFeedback.push(pivot);
@@ -2285,12 +2710,12 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2285
2710
  actions.push(candidateAction);
2286
2711
  results.push(result);
2287
2712
  if (activeIntents.length > 0) {
2288
- const touchedHandle = normalizeHandle4(
2713
+ const touchedHandle = normalizeHandle5(
2289
2714
  candidateAction.targetHandle ?? candidateAction.handle ?? (candidateAction.tweetId ? authorByTweetId.get(candidateAction.tweetId) : void 0)
2290
2715
  );
2291
2716
  const loweredContent = (candidateAction.content ?? "").toLowerCase();
2292
2717
  for (const intent of activeIntents) {
2293
- const handleMatch = touchedHandle.length > 0 && intent.targetHandles.some((handle) => normalizeHandle4(handle) === touchedHandle);
2718
+ const handleMatch = touchedHandle.length > 0 && intent.targetHandles.some((handle) => normalizeHandle5(handle) === touchedHandle);
2294
2719
  const topicMatch = loweredContent.length > 0 && intent.focusTopics.some((topic) => loweredContent.includes(topic.toLowerCase()));
2295
2720
  if (!handleMatch && !topicMatch) continue;
2296
2721
  recordIntentExecution(intent.id, {
@@ -2305,10 +2730,16 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2305
2730
  blockedTweetIds.add(candidateAction.tweetId);
2306
2731
  }
2307
2732
  if (candidateAction.action === "reply" && /duplicate content/i.test(err)) {
2308
- disallowedActions.add("reply");
2309
- const reason = "Reply failed with duplicate-content error. Switch to non-reply actions.";
2310
- policyFeedback.push(reason);
2311
- logger.info(`Policy adjustment: ${reason}`);
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
+ }
2312
2743
  }
2313
2744
  if ((candidateAction.action === "post" || candidateAction.action === "schedule") && /duplicate content/i.test(err)) {
2314
2745
  const reason = "Write-path duplicate-content failure. Change framing and try a different angle.";
@@ -2335,4 +2766,4 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
2335
2766
  export {
2336
2767
  runAutonomyCycle
2337
2768
  };
2338
- //# sourceMappingURL=chunk-HXI2EH5C.js.map
2769
+ //# sourceMappingURL=chunk-TTM54LQR.js.map