spora 0.7.9 → 0.7.11

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.
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  runAutonomyCycle
3
- } from "./chunk-TTDQZI5W.js";
3
+ } from "./chunk-3S44SIUP.js";
4
4
  import "./chunk-TKGB5LIN.js";
5
5
  import "./chunk-AIGSCHZK.js";
6
6
  import "./chunk-73CWOI44.js";
@@ -17,4 +17,4 @@ import "./chunk-ZWKTKWS6.js";
17
17
  export {
18
18
  runAutonomyCycle
19
19
  };
20
- //# sourceMappingURL=autonomy-ZMFZRXDZ.js.map
20
+ //# sourceMappingURL=autonomy-ZVXD2OP2.js.map
@@ -967,6 +967,37 @@ function canonicalPlannerHandle(handle) {
967
967
  function canonicalizePlannerQuery(rawQuery) {
968
968
  return rawQuery.replace(/from:([a-zA-Z0-9_]{1,15})/gi, (_match, handle) => `from:${canonicalPlannerHandle(handle)}`);
969
969
  }
970
+ var REPLY_TARGET_COOLDOWN_MS = 6 * 60 * 60 * 1e3;
971
+ function normalizeDraftText(text) {
972
+ return text.toLowerCase().replace(/https?:\/\/\S+/g, "").replace(/[@#][a-z0-9_]+/g, "").replace(/[^a-z0-9\s]/g, " ").replace(/\s+/g, " ").trim();
973
+ }
974
+ function tokenJaccard(a, b) {
975
+ const aTokens = new Set(normalizeDraftText(a).split(" ").filter(Boolean));
976
+ const bTokens = new Set(normalizeDraftText(b).split(" ").filter(Boolean));
977
+ if (aTokens.size === 0 || bTokens.size === 0) return 0;
978
+ let overlap = 0;
979
+ for (const token of aTokens) {
980
+ if (bTokens.has(token)) overlap += 1;
981
+ }
982
+ const union = aTokens.size + bTokens.size - overlap;
983
+ return union > 0 ? overlap / union : 0;
984
+ }
985
+ function nearDuplicateDraft(content, previous) {
986
+ const normalized = normalizeDraftText(content);
987
+ if (!normalized) return false;
988
+ return previous.some((sample) => {
989
+ const candidate = normalizeDraftText(sample);
990
+ if (!candidate) return false;
991
+ if (candidate === normalized) return true;
992
+ const sameOpening = normalized.split(" ").slice(0, 7).join(" ") === candidate.split(" ").slice(0, 7).join(" ");
993
+ if (sameOpening) return true;
994
+ return tokenJaccard(normalized, candidate) >= 0.86;
995
+ });
996
+ }
997
+ function shouldHardBlockPolicyReason(reason) {
998
+ const lower = reason.toLowerCase();
999
+ return lower.includes("already executed for tweet") || lower.includes("rejected self-interaction") || lower.includes("self-follow") || lower.includes("requires replying only") || lower.includes("reply target must be one of") || lower.includes("interaction target must be one of") || lower.includes("forbids standalone original posts") || lower.includes("reply-only mode");
1000
+ }
970
1001
  function roughWordCount(text) {
971
1002
  return text.trim().split(/\s+/).filter(Boolean).length;
972
1003
  }
@@ -1083,6 +1114,45 @@ async function rewriteDraftForHumanVoice(input) {
1083
1114
  return null;
1084
1115
  }
1085
1116
  }
1117
+ async function composeHeartbeatPostDraft(input) {
1118
+ const system = [
1119
+ `You write one original X post for ${input.identityName} (@${input.identityHandle}).`,
1120
+ "Write like a real person: natural, concise, specific.",
1121
+ "No manifesto voice, no generic AI lecture style.",
1122
+ "Return ONLY the post text."
1123
+ ].join(" ");
1124
+ const promptParts = [];
1125
+ promptParts.push(`Tone: ${input.tone || "natural"}`);
1126
+ promptParts.push(`Goals: ${input.goals.slice(0, 4).join(" | ") || "none"}`);
1127
+ promptParts.push(`Boundaries: ${input.boundaries.slice(0, 4).join(" | ") || "none"}`);
1128
+ promptParts.push("Constraints:");
1129
+ promptParts.push("- 18-180 characters");
1130
+ promptParts.push("- 1-2 short sentences");
1131
+ promptParts.push("- concrete and human sounding");
1132
+ promptParts.push("- don't start with @ unless absolutely needed");
1133
+ promptParts.push("- avoid abstract 'the real question' framing");
1134
+ if (input.contextTweets.length > 0) {
1135
+ promptParts.push("Current timeline context:");
1136
+ for (const tweet of input.contextTweets.slice(0, 6)) {
1137
+ promptParts.push(`- @${tweet.authorHandle}: ${tweet.text.slice(0, 160)}`);
1138
+ }
1139
+ }
1140
+ if (input.recentSamples.length > 0) {
1141
+ promptParts.push("Avoid repeating these recent lines:");
1142
+ for (const sample of input.recentSamples.slice(0, 5)) {
1143
+ promptParts.push(`- ${sample.slice(0, 120)}`);
1144
+ }
1145
+ }
1146
+ try {
1147
+ const response = await generateResponse(system, promptParts.join("\n"));
1148
+ const candidate = cleanRewriteOutput(response.content);
1149
+ if (!candidate) return null;
1150
+ if (candidate.length < 18 || candidate.length > 220) return null;
1151
+ return candidate;
1152
+ } catch {
1153
+ return null;
1154
+ }
1155
+ }
1086
1156
  function loopClampInt(value, min, max) {
1087
1157
  return Math.max(min, Math.min(max, Math.round(value)));
1088
1158
  }
@@ -1259,6 +1329,10 @@ function buildToolLoopPrompt(input) {
1259
1329
  for (const line of input.constraintLines) lines.push(line);
1260
1330
  lines.push("");
1261
1331
  }
1332
+ if (input.heartbeatDirective) {
1333
+ lines.push(`Heartbeat directive: ${input.heartbeatDirective}`);
1334
+ lines.push("");
1335
+ }
1262
1336
  if (input.recentMemory.length > 0) {
1263
1337
  lines.push("Recent memory:");
1264
1338
  for (const row of input.recentMemory.slice(0, 8)) {
@@ -1270,6 +1344,21 @@ function buildToolLoopPrompt(input) {
1270
1344
  lines.push(
1271
1345
  `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
1346
  );
1347
+ if (input.usedReplyTargets.length > 0) {
1348
+ lines.push(`Reply targets already used this heartbeat: ${input.usedReplyTargets.slice(-8).join(", ")}`);
1349
+ }
1350
+ if (input.recentReplyTargets.length > 0) {
1351
+ lines.push(`Recently replied targets (cooldown): ${input.recentReplyTargets.slice(0, 8).join(", ")}`);
1352
+ }
1353
+ if (input.blockedReplyTargets.length > 0) {
1354
+ lines.push(`Temporarily blocked reply targets this heartbeat: ${input.blockedReplyTargets.slice(0, 8).join(", ")}`);
1355
+ }
1356
+ if (input.recentWrittenSamples.length > 0) {
1357
+ lines.push("Recent writing to avoid repeating:");
1358
+ for (const sample of input.recentWrittenSamples.slice(0, 4)) {
1359
+ lines.push(`- ${sample.slice(0, 100)}`);
1360
+ }
1361
+ }
1273
1362
  lines.push("");
1274
1363
  if (input.toolHistory.length > 0) {
1275
1364
  lines.push("Recent tool steps:");
@@ -1316,6 +1405,8 @@ function buildToolLoopPrompt(input) {
1316
1405
  lines.push("- if timeline/mentions already have viable tweets, prefer acting over more research");
1317
1406
  lines.push("- use search/profile checks only when you have a clear reason");
1318
1407
  lines.push("- avoid repeating the same query/target unless context changed");
1408
+ lines.push("- one reply per target tweet per heartbeat, always choose a new tweet next");
1409
+ lines.push("- keep timeline presence alive: post an original thought regularly, not only replies");
1319
1410
  lines.push("");
1320
1411
  lines.push("Writing constraints:");
1321
1412
  lines.push("- sound human and specific, no manifesto language");
@@ -1374,6 +1465,10 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1374
1465
  const client = await getXClient();
1375
1466
  const identity = loadIdentity();
1376
1467
  const constraints = getPersonaConstraints();
1468
+ const strictReplyHandles = new Set(constraints.onlyReplyToHandles.map((handle) => normalizeHandle2(handle)));
1469
+ const strictInteractHandles = new Set(
1470
+ [...constraints.onlyReplyToHandles, ...constraints.onlyInteractWithHandles].map((handle) => normalizeHandle2(handle))
1471
+ );
1377
1472
  const constraintLines = buildPersonaConstraintLines(constraints);
1378
1473
  if (constraintLines.length > 0) {
1379
1474
  logger.info(`Persona constraints active: ${constraintLines.join(" | ")}`);
@@ -1432,9 +1527,99 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1432
1527
  const activeIntents = listIntents();
1433
1528
  let staleSteps = 0;
1434
1529
  let attemptedActions = 0;
1435
- const maxToolSteps = Math.max(12, maxActions * 6);
1530
+ const maxConfiguredActions = Math.max(1, maxActions);
1531
+ let actionBudget = maxConfiguredActions;
1532
+ if (constraints.replyOnlyMode || strictReplyHandles.size > 0) {
1533
+ actionBudget = 1;
1534
+ } else {
1535
+ const upper = Math.min(maxConfiguredActions, 3);
1536
+ actionBudget = Math.max(1, Math.floor(Math.random() * upper) + 1);
1537
+ }
1538
+ const maxToolSteps = Math.max(8, actionBudget * 5);
1539
+ const recentInteractions = getRecentInteractions(220);
1540
+ const now = Date.now();
1541
+ const recentReplyTargets = new Set(
1542
+ recentInteractions.filter((entry) => entry.type === "reply" && typeof entry.inReplyTo === "string").filter((entry) => {
1543
+ const ts = Date.parse(entry.timestamp);
1544
+ return Number.isNaN(ts) || now - ts <= REPLY_TARGET_COOLDOWN_MS;
1545
+ }).map((entry) => entry.inReplyTo)
1546
+ );
1547
+ const recentWrittenSamples = recentInteractions.filter((entry) => (entry.type === "post" || entry.type === "reply") && typeof entry.content === "string").map((entry) => entry.content.trim()).filter(Boolean).slice(0, 16);
1548
+ const recentPostCount = recentInteractions.filter((entry) => entry.type === "post").slice(0, 30).length;
1549
+ const recentReplyCount = recentInteractions.filter((entry) => entry.type === "reply").slice(0, 30).length;
1550
+ const postStarved = recentPostCount === 0 || recentReplyCount >= recentPostCount * 3 + 2;
1551
+ const postFirstMode = !constraints.noOriginalPosts && !constraints.replyOnlyMode && (postStarved || heartbeatCount % 4 === 0);
1552
+ const minPostsThisHeartbeat = postFirstMode ? 1 : 0;
1553
+ if (postFirstMode) {
1554
+ policyFeedback.push("Heartbeat objective: publish one original post first, then engage.");
1555
+ }
1556
+ const usedReplyTargets = /* @__PURE__ */ new Set();
1557
+ const blockedReplyTargets = /* @__PURE__ */ new Set();
1558
+ const writtenThisHeartbeat = [];
1559
+ let blockedReplyStreak = 0;
1560
+ let forcePostNextStep = false;
1436
1561
  getRecentInteractions(20);
1437
- for (let step = 0; step < maxToolSteps && actions.length < maxActions; step += 1) {
1562
+ const markNoProgress = (note) => {
1563
+ if (note) policyFeedback.push(note);
1564
+ staleSteps += 1;
1565
+ if (staleSteps >= 5) {
1566
+ logger.info("Tool loop stopped after repeated blocked/no-progress steps.");
1567
+ return true;
1568
+ }
1569
+ return false;
1570
+ };
1571
+ const makeForcedPostDecision = async (reason, contextTweets) => {
1572
+ if (constraints.noOriginalPosts || constraints.replyOnlyMode || strictReplyHandles.size > 0) {
1573
+ return null;
1574
+ }
1575
+ const forcedDraft = await composeHeartbeatPostDraft({
1576
+ identityName: identity.name,
1577
+ identityHandle: identity.handle,
1578
+ tone: identity.tone,
1579
+ goals: identity.goals ?? [],
1580
+ boundaries: identity.boundaries ?? [],
1581
+ contextTweets: contextTweets.slice(0, 8),
1582
+ recentSamples: [...writtenThisHeartbeat, ...recentWrittenSamples].slice(0, 8)
1583
+ });
1584
+ if (!forcedDraft) return null;
1585
+ return {
1586
+ tool: "post",
1587
+ args: { content: forcedDraft },
1588
+ reasoning: reason
1589
+ };
1590
+ };
1591
+ const hasEligibleStrictReplyTarget = () => {
1592
+ const observed = collectObserved(state);
1593
+ return observed.tweets.some((tweet) => {
1594
+ const handle = normalizeHandle2(tweet.authorHandle);
1595
+ return strictReplyHandles.has(handle) && !usedReplyTargets.has(tweet.id) && !recentReplyTargets.has(tweet.id) && !blockedReplyTargets.has(tweet.id);
1596
+ });
1597
+ };
1598
+ const onBlockedReplyDecision = (reason, tweetId) => {
1599
+ policyFeedback.push(reason);
1600
+ logger.info(`Planner loop guard: ${reason}`);
1601
+ if (tweetId) blockedReplyTargets.add(tweetId);
1602
+ blockedReplyStreak += 1;
1603
+ if (blockedReplyStreak >= 2) {
1604
+ if (!constraints.noOriginalPosts && !constraints.replyOnlyMode && strictReplyHandles.size === 0) {
1605
+ forcePostNextStep = true;
1606
+ blockedReplyStreak = 0;
1607
+ policyFeedback.push("Reply lane is saturated; forcing one original post next.");
1608
+ logger.info("Planner loop guard: reply lane saturated, forcing post pivot.");
1609
+ return false;
1610
+ }
1611
+ policyFeedback.push("Ending loop after repeated blocked reply targets this heartbeat.");
1612
+ logger.info("Planner loop ended after repeated blocked reply targets.");
1613
+ return true;
1614
+ }
1615
+ return false;
1616
+ };
1617
+ for (let step = 0; step < maxToolSteps && actions.length < actionBudget; step += 1) {
1618
+ if (strictReplyHandles.size > 0 && !hasEligibleStrictReplyTarget()) {
1619
+ policyFeedback.push("No eligible fresh target tweets remain for strict reply mode this heartbeat.");
1620
+ logger.info("Planner loop ended: no eligible strict-reply targets left.");
1621
+ break;
1622
+ }
1438
1623
  const candidates = buildPlannerCandidates(state);
1439
1624
  const recentMemory = getRecentInteractions(24).slice(0, 12).map((entry) => {
1440
1625
  const text = (entry.content ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
@@ -1457,6 +1642,11 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1457
1642
  policyFeedback,
1458
1643
  toolHistory,
1459
1644
  recentMemory,
1645
+ usedReplyTargets: [...usedReplyTargets],
1646
+ recentReplyTargets: [...recentReplyTargets],
1647
+ blockedReplyTargets: [...blockedReplyTargets],
1648
+ recentWrittenSamples: [...writtenThisHeartbeat, ...recentWrittenSamples].slice(0, 8),
1649
+ heartbeatDirective: postFirstMode && actions.length === 0 ? "Start with an original post this cycle." : void 0,
1460
1650
  heartbeatCount
1461
1651
  });
1462
1652
  let decision = fallbackPlannerDecision(step, candidates.length);
@@ -1471,6 +1661,35 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1471
1661
  } catch (error) {
1472
1662
  policyFeedback.push(`Planner call failed: ${error.message}`);
1473
1663
  }
1664
+ if (forcePostNextStep && decision.tool !== "post") {
1665
+ const forced = await makeForcedPostDecision("reply-lane saturation pivot", candidates);
1666
+ if (forced) {
1667
+ decision = forced;
1668
+ forcePostNextStep = false;
1669
+ policyFeedback.push("Planner adjusted: switched to an original post after reply loop saturation.");
1670
+ }
1671
+ }
1672
+ if (step === 0 && actions.length === 0 && postFirstMode && decision.tool !== "post") {
1673
+ const forced = await makeForcedPostDecision("post-first heartbeat mode", candidates);
1674
+ if (forced) {
1675
+ decision = forced;
1676
+ policyFeedback.push("Planner adjusted: posting first this heartbeat to keep timeline presence.");
1677
+ }
1678
+ }
1679
+ if (decision.tool !== "post") {
1680
+ const postsSoFar = actions.filter((a) => a.action === "post").length;
1681
+ const repliesSoFar = actions.filter((a) => a.action === "reply").length;
1682
+ const postsRemaining = Math.max(0, minPostsThisHeartbeat - postsSoFar);
1683
+ const remainingSlots = actionBudget - actions.length;
1684
+ const shouldShiftToPost = postsRemaining > 0 && (remainingSlots <= postsRemaining || postFirstMode && postsSoFar === 0 && repliesSoFar >= 1);
1685
+ if (shouldShiftToPost) {
1686
+ const forced = await makeForcedPostDecision("post-balance safeguard", candidates);
1687
+ if (forced) {
1688
+ decision = forced;
1689
+ policyFeedback.push("Planner adjusted: balancing heartbeat mix with an original post.");
1690
+ }
1691
+ }
1692
+ }
1474
1693
  logger.info(`Tool loop step ${step + 1}: ${decision.tool}`);
1475
1694
  let stepProgress = false;
1476
1695
  if (decision.tool === "observe_timeline") {
@@ -1488,7 +1707,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1488
1707
  } catch (error) {
1489
1708
  policyFeedback.push(`observe_timeline failed: ${error.message}`);
1490
1709
  }
1491
- if (!stepProgress) policyFeedback.push("observe_timeline found no new tweets.");
1710
+ if (!stepProgress && markNoProgress("observe_timeline found no new tweets.")) break;
1492
1711
  continue;
1493
1712
  }
1494
1713
  if (decision.tool === "observe_mentions") {
@@ -1506,13 +1725,13 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1506
1725
  } catch (error) {
1507
1726
  policyFeedback.push(`observe_mentions failed: ${error.message}`);
1508
1727
  }
1509
- if (!stepProgress) policyFeedback.push("observe_mentions found no new tweets.");
1728
+ if (!stepProgress && markNoProgress("observe_mentions found no new tweets.")) break;
1510
1729
  continue;
1511
1730
  }
1512
1731
  if (decision.tool === "search_tweets") {
1513
1732
  const rawQuery = argString(decision.args, "query");
1514
1733
  if (!rawQuery) {
1515
- policyFeedback.push("search_tweets rejected: missing query.");
1734
+ if (markNoProgress("search_tweets rejected: missing query.")) break;
1516
1735
  continue;
1517
1736
  }
1518
1737
  const query = canonicalizePlannerQuery(rawQuery);
@@ -1534,14 +1753,14 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1534
1753
  } catch (error) {
1535
1754
  policyFeedback.push(`search_tweets failed for "${query}": ${error.message}`);
1536
1755
  }
1537
- if (!stepProgress) policyFeedback.push(`search_tweets "${query}" found no new tweets.`);
1756
+ if (!stepProgress && markNoProgress(`search_tweets "${query}" found no new tweets.`)) break;
1538
1757
  continue;
1539
1758
  }
1540
1759
  if (decision.tool === "check_profile") {
1541
1760
  const handleInput = argString(decision.args, "handle");
1542
1761
  const handle = handleInput ? canonicalPlannerHandle(handleInput) : "";
1543
1762
  if (!handle) {
1544
- policyFeedback.push("check_profile rejected: missing handle.");
1763
+ if (markNoProgress("check_profile rejected: missing handle.")) break;
1545
1764
  continue;
1546
1765
  }
1547
1766
  const count = argCount(decision.args, 6, 3, 12);
@@ -1565,14 +1784,13 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1565
1784
  } catch (error) {
1566
1785
  policyFeedback.push(`check_profile failed for @${handle}: ${error.message}`);
1567
1786
  }
1568
- if (!stepProgress) policyFeedback.push(`check_profile @${handle} found no new tweets.`);
1787
+ if (!stepProgress && markNoProgress(`check_profile @${handle} found no new tweets.`)) break;
1569
1788
  continue;
1570
1789
  }
1571
1790
  const observed = collectObserved(state);
1572
1791
  let candidateAction = decisionToAgentAction(decision);
1573
1792
  if (!candidateAction) {
1574
- policyFeedback.push(`Tool decision ${decision.tool} rejected: invalid arguments.`);
1575
- staleSteps += 1;
1793
+ if (markNoProgress(`Tool decision ${decision.tool} rejected: invalid arguments.`)) break;
1576
1794
  continue;
1577
1795
  }
1578
1796
  if (candidateAction.tweetId && observed.byId.has(candidateAction.tweetId)) {
@@ -1581,6 +1799,9 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1581
1799
  candidateAction.targetHandle = `@${author}`;
1582
1800
  }
1583
1801
  }
1802
+ const actionTargetHandle = normalizeHandle2(
1803
+ candidateAction.targetHandle ?? candidateAction.handle ?? (candidateAction.tweetId ? observed.byId.get(candidateAction.tweetId)?.authorHandle : void 0)
1804
+ );
1584
1805
  if (candidateAction.action === "skip") {
1585
1806
  const reason = candidateAction.reason ?? candidateAction.reasoning ?? "planner skip";
1586
1807
  policyFeedback.push(`Planner chose skip: ${reason}`);
@@ -1589,10 +1810,71 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1589
1810
  tool: decision.tool,
1590
1811
  summary: reason
1591
1812
  });
1592
- staleSteps += 1;
1593
- if (staleSteps >= 4) break;
1813
+ if (markNoProgress()) break;
1594
1814
  continue;
1595
1815
  }
1816
+ if (constraints.noOriginalPosts && (candidateAction.action === "post" || candidateAction.action === "schedule")) {
1817
+ const reason = "Original posts are disabled by persona constraints.";
1818
+ policyFeedback.push(reason);
1819
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1820
+ if (markNoProgress()) break;
1821
+ continue;
1822
+ }
1823
+ if (strictReplyHandles.size > 0) {
1824
+ if (candidateAction.action !== "reply") {
1825
+ const reason = `Only reply actions are allowed for this persona target: @${[...strictReplyHandles].join(", @")}.`;
1826
+ policyFeedback.push(reason);
1827
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1828
+ if (markNoProgress()) break;
1829
+ continue;
1830
+ }
1831
+ if (!actionTargetHandle || !strictReplyHandles.has(actionTargetHandle)) {
1832
+ const reason = `Reply target must be one of @${[...strictReplyHandles].join(", @")}.`;
1833
+ policyFeedback.push(reason);
1834
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1835
+ if (markNoProgress()) break;
1836
+ continue;
1837
+ }
1838
+ }
1839
+ if (strictInteractHandles.size > 0 && ["reply", "like", "retweet", "follow"].includes(candidateAction.action)) {
1840
+ if (!actionTargetHandle || !strictInteractHandles.has(actionTargetHandle)) {
1841
+ const reason = `Interaction target must be one of @${[...strictInteractHandles].join(", @")}.`;
1842
+ policyFeedback.push(reason);
1843
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1844
+ if (markNoProgress()) break;
1845
+ continue;
1846
+ }
1847
+ }
1848
+ if (candidateAction.action === "reply" && candidateAction.tweetId) {
1849
+ if (blockedReplyTargets.has(candidateAction.tweetId)) {
1850
+ const reason = `Reply target ${candidateAction.tweetId} is temporarily blocked this heartbeat. Pick a different tweet.`;
1851
+ if (onBlockedReplyDecision(reason, candidateAction.tweetId)) break;
1852
+ if (markNoProgress()) break;
1853
+ continue;
1854
+ }
1855
+ if (usedReplyTargets.has(candidateAction.tweetId)) {
1856
+ const reason = `Reply target ${candidateAction.tweetId} already used this heartbeat. Pick a different tweet.`;
1857
+ if (onBlockedReplyDecision(reason, candidateAction.tweetId)) break;
1858
+ if (markNoProgress()) break;
1859
+ continue;
1860
+ }
1861
+ if (recentReplyTargets.has(candidateAction.tweetId)) {
1862
+ const reason = `Reply target ${candidateAction.tweetId} is in cooldown from recent history. Pick a fresher target.`;
1863
+ if (onBlockedReplyDecision(reason, candidateAction.tweetId)) break;
1864
+ if (markNoProgress()) break;
1865
+ continue;
1866
+ }
1867
+ }
1868
+ if ((candidateAction.action === "reply" || candidateAction.action === "post") && candidateAction.content) {
1869
+ const seenDrafts = [...writtenThisHeartbeat, ...recentWrittenSamples].slice(0, 18);
1870
+ if (nearDuplicateDraft(candidateAction.content, seenDrafts)) {
1871
+ const reason = "Draft is too similar to recent writing. Try a new angle before posting.";
1872
+ policyFeedback.push(reason);
1873
+ logger.info(`Planner loop guard: ${reason}`);
1874
+ if (markNoProgress()) break;
1875
+ continue;
1876
+ }
1877
+ }
1596
1878
  attemptedActions += 1;
1597
1879
  let policy = evaluateActionPolicy({
1598
1880
  action: candidateAction,
@@ -1634,12 +1916,28 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1634
1916
  }
1635
1917
  if (!policy.allowed) {
1636
1918
  const reason = policy.reason ?? "Policy rejected action";
1919
+ if (shouldHardBlockPolicyReason(reason)) {
1920
+ policyFeedback.push(`Policy blocked action: ${reason}`);
1921
+ logger.info(`Policy blocked ${candidateAction.action}: ${reason}`);
1922
+ if (markNoProgress()) break;
1923
+ continue;
1924
+ }
1637
1925
  policyFeedback.push(`Policy advisory (not blocking): ${reason}`);
1638
1926
  logger.info(`Policy advisory for ${candidateAction.action}: ${reason}`);
1639
1927
  }
1928
+ if (candidateAction.action === "reply" && candidateAction.tweetId) {
1929
+ usedReplyTargets.add(candidateAction.tweetId);
1930
+ }
1931
+ if ((candidateAction.action === "reply" || candidateAction.action === "post") && candidateAction.content) {
1932
+ writtenThisHeartbeat.unshift(candidateAction.content.trim());
1933
+ if (writtenThisHeartbeat.length > 16) {
1934
+ writtenThisHeartbeat.length = 16;
1935
+ }
1936
+ }
1640
1937
  const result = await executeAction(candidateAction);
1641
1938
  actions.push(candidateAction);
1642
1939
  results.push(result);
1940
+ blockedReplyStreak = 0;
1643
1941
  staleSteps = 0;
1644
1942
  stepProgress = true;
1645
1943
  toolHistory.push({
@@ -1667,6 +1965,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1667
1965
  if (candidateAction.action === "reply" && /duplicate content/i.test(err)) {
1668
1966
  const reason = "Reply failed with duplicate-content error. Planner should choose a different wording/target.";
1669
1967
  policyFeedback.push(reason);
1968
+ if (candidateAction.tweetId) blockedReplyTargets.add(candidateAction.tweetId);
1670
1969
  logger.info(`Policy adjustment: ${reason}`);
1671
1970
  }
1672
1971
  if (candidateAction.action === "post" && /duplicate content/i.test(err)) {
@@ -1675,11 +1974,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1675
1974
  logger.info(`Policy adjustment: ${reason}`);
1676
1975
  }
1677
1976
  }
1678
- if (!stepProgress) {
1679
- staleSteps += 1;
1680
- }
1681
- if (staleSteps >= 5) {
1682
- logger.info("Tool loop stopped after repeated no-progress steps.");
1977
+ if (!stepProgress && markNoProgress()) {
1683
1978
  break;
1684
1979
  }
1685
1980
  }
@@ -1696,4 +1991,4 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1696
1991
  export {
1697
1992
  runAutonomyCycle
1698
1993
  };
1699
- //# sourceMappingURL=chunk-TTDQZI5W.js.map
1994
+ //# sourceMappingURL=chunk-3S44SIUP.js.map