spora 0.7.9 → 0.7.10

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-245JDXK4.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-YBWBP6QJ.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
  }
@@ -1270,6 +1301,21 @@ function buildToolLoopPrompt(input) {
1270
1301
  lines.push(
1271
1302
  `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
1303
  );
1304
+ if (input.usedReplyTargets.length > 0) {
1305
+ lines.push(`Reply targets already used this heartbeat: ${input.usedReplyTargets.slice(-8).join(", ")}`);
1306
+ }
1307
+ if (input.recentReplyTargets.length > 0) {
1308
+ lines.push(`Recently replied targets (cooldown): ${input.recentReplyTargets.slice(0, 8).join(", ")}`);
1309
+ }
1310
+ if (input.blockedReplyTargets.length > 0) {
1311
+ lines.push(`Temporarily blocked reply targets this heartbeat: ${input.blockedReplyTargets.slice(0, 8).join(", ")}`);
1312
+ }
1313
+ if (input.recentWrittenSamples.length > 0) {
1314
+ lines.push("Recent writing to avoid repeating:");
1315
+ for (const sample of input.recentWrittenSamples.slice(0, 4)) {
1316
+ lines.push(`- ${sample.slice(0, 100)}`);
1317
+ }
1318
+ }
1273
1319
  lines.push("");
1274
1320
  if (input.toolHistory.length > 0) {
1275
1321
  lines.push("Recent tool steps:");
@@ -1316,6 +1362,7 @@ function buildToolLoopPrompt(input) {
1316
1362
  lines.push("- if timeline/mentions already have viable tweets, prefer acting over more research");
1317
1363
  lines.push("- use search/profile checks only when you have a clear reason");
1318
1364
  lines.push("- avoid repeating the same query/target unless context changed");
1365
+ lines.push("- one reply per target tweet per heartbeat, always choose a new tweet next");
1319
1366
  lines.push("");
1320
1367
  lines.push("Writing constraints:");
1321
1368
  lines.push("- sound human and specific, no manifesto language");
@@ -1374,6 +1421,10 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1374
1421
  const client = await getXClient();
1375
1422
  const identity = loadIdentity();
1376
1423
  const constraints = getPersonaConstraints();
1424
+ const strictReplyHandles = new Set(constraints.onlyReplyToHandles.map((handle) => normalizeHandle2(handle)));
1425
+ const strictInteractHandles = new Set(
1426
+ [...constraints.onlyReplyToHandles, ...constraints.onlyInteractWithHandles].map((handle) => normalizeHandle2(handle))
1427
+ );
1377
1428
  const constraintLines = buildPersonaConstraintLines(constraints);
1378
1429
  if (constraintLines.length > 0) {
1379
1430
  logger.info(`Persona constraints active: ${constraintLines.join(" | ")}`);
@@ -1432,9 +1483,50 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1432
1483
  const activeIntents = listIntents();
1433
1484
  let staleSteps = 0;
1434
1485
  let attemptedActions = 0;
1435
- const maxToolSteps = Math.max(12, maxActions * 6);
1486
+ const maxConfiguredActions = Math.max(1, maxActions);
1487
+ let actionBudget = maxConfiguredActions;
1488
+ if (constraints.replyOnlyMode || strictReplyHandles.size > 0) {
1489
+ actionBudget = 1;
1490
+ } else {
1491
+ const upper = Math.min(maxConfiguredActions, 3);
1492
+ actionBudget = Math.max(1, Math.floor(Math.random() * upper) + 1);
1493
+ }
1494
+ const maxToolSteps = Math.max(8, actionBudget * 5);
1495
+ const recentInteractions = getRecentInteractions(220);
1496
+ const now = Date.now();
1497
+ const recentReplyTargets = new Set(
1498
+ recentInteractions.filter((entry) => entry.type === "reply" && typeof entry.inReplyTo === "string").filter((entry) => {
1499
+ const ts = Date.parse(entry.timestamp);
1500
+ return Number.isNaN(ts) || now - ts <= REPLY_TARGET_COOLDOWN_MS;
1501
+ }).map((entry) => entry.inReplyTo)
1502
+ );
1503
+ 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);
1504
+ const usedReplyTargets = /* @__PURE__ */ new Set();
1505
+ const blockedReplyTargets = /* @__PURE__ */ new Set();
1506
+ const writtenThisHeartbeat = [];
1436
1507
  getRecentInteractions(20);
1437
- for (let step = 0; step < maxToolSteps && actions.length < maxActions; step += 1) {
1508
+ const markNoProgress = (note) => {
1509
+ if (note) policyFeedback.push(note);
1510
+ staleSteps += 1;
1511
+ if (staleSteps >= 5) {
1512
+ logger.info("Tool loop stopped after repeated blocked/no-progress steps.");
1513
+ return true;
1514
+ }
1515
+ return false;
1516
+ };
1517
+ const hasEligibleStrictReplyTarget = () => {
1518
+ const observed = collectObserved(state);
1519
+ return observed.tweets.some((tweet) => {
1520
+ const handle = normalizeHandle2(tweet.authorHandle);
1521
+ return strictReplyHandles.has(handle) && !usedReplyTargets.has(tweet.id) && !recentReplyTargets.has(tweet.id) && !blockedReplyTargets.has(tweet.id);
1522
+ });
1523
+ };
1524
+ for (let step = 0; step < maxToolSteps && actions.length < actionBudget; step += 1) {
1525
+ if (strictReplyHandles.size > 0 && !hasEligibleStrictReplyTarget()) {
1526
+ policyFeedback.push("No eligible fresh target tweets remain for strict reply mode this heartbeat.");
1527
+ logger.info("Planner loop ended: no eligible strict-reply targets left.");
1528
+ break;
1529
+ }
1438
1530
  const candidates = buildPlannerCandidates(state);
1439
1531
  const recentMemory = getRecentInteractions(24).slice(0, 12).map((entry) => {
1440
1532
  const text = (entry.content ?? "").replace(/\s+/g, " ").trim().slice(0, 120);
@@ -1457,6 +1549,10 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1457
1549
  policyFeedback,
1458
1550
  toolHistory,
1459
1551
  recentMemory,
1552
+ usedReplyTargets: [...usedReplyTargets],
1553
+ recentReplyTargets: [...recentReplyTargets],
1554
+ blockedReplyTargets: [...blockedReplyTargets],
1555
+ recentWrittenSamples: [...writtenThisHeartbeat, ...recentWrittenSamples].slice(0, 8),
1460
1556
  heartbeatCount
1461
1557
  });
1462
1558
  let decision = fallbackPlannerDecision(step, candidates.length);
@@ -1488,7 +1584,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1488
1584
  } catch (error) {
1489
1585
  policyFeedback.push(`observe_timeline failed: ${error.message}`);
1490
1586
  }
1491
- if (!stepProgress) policyFeedback.push("observe_timeline found no new tweets.");
1587
+ if (!stepProgress && markNoProgress("observe_timeline found no new tweets.")) break;
1492
1588
  continue;
1493
1589
  }
1494
1590
  if (decision.tool === "observe_mentions") {
@@ -1506,13 +1602,13 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1506
1602
  } catch (error) {
1507
1603
  policyFeedback.push(`observe_mentions failed: ${error.message}`);
1508
1604
  }
1509
- if (!stepProgress) policyFeedback.push("observe_mentions found no new tweets.");
1605
+ if (!stepProgress && markNoProgress("observe_mentions found no new tweets.")) break;
1510
1606
  continue;
1511
1607
  }
1512
1608
  if (decision.tool === "search_tweets") {
1513
1609
  const rawQuery = argString(decision.args, "query");
1514
1610
  if (!rawQuery) {
1515
- policyFeedback.push("search_tweets rejected: missing query.");
1611
+ if (markNoProgress("search_tweets rejected: missing query.")) break;
1516
1612
  continue;
1517
1613
  }
1518
1614
  const query = canonicalizePlannerQuery(rawQuery);
@@ -1534,14 +1630,14 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1534
1630
  } catch (error) {
1535
1631
  policyFeedback.push(`search_tweets failed for "${query}": ${error.message}`);
1536
1632
  }
1537
- if (!stepProgress) policyFeedback.push(`search_tweets "${query}" found no new tweets.`);
1633
+ if (!stepProgress && markNoProgress(`search_tweets "${query}" found no new tweets.`)) break;
1538
1634
  continue;
1539
1635
  }
1540
1636
  if (decision.tool === "check_profile") {
1541
1637
  const handleInput = argString(decision.args, "handle");
1542
1638
  const handle = handleInput ? canonicalPlannerHandle(handleInput) : "";
1543
1639
  if (!handle) {
1544
- policyFeedback.push("check_profile rejected: missing handle.");
1640
+ if (markNoProgress("check_profile rejected: missing handle.")) break;
1545
1641
  continue;
1546
1642
  }
1547
1643
  const count = argCount(decision.args, 6, 3, 12);
@@ -1565,14 +1661,13 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1565
1661
  } catch (error) {
1566
1662
  policyFeedback.push(`check_profile failed for @${handle}: ${error.message}`);
1567
1663
  }
1568
- if (!stepProgress) policyFeedback.push(`check_profile @${handle} found no new tweets.`);
1664
+ if (!stepProgress && markNoProgress(`check_profile @${handle} found no new tweets.`)) break;
1569
1665
  continue;
1570
1666
  }
1571
1667
  const observed = collectObserved(state);
1572
1668
  let candidateAction = decisionToAgentAction(decision);
1573
1669
  if (!candidateAction) {
1574
- policyFeedback.push(`Tool decision ${decision.tool} rejected: invalid arguments.`);
1575
- staleSteps += 1;
1670
+ if (markNoProgress(`Tool decision ${decision.tool} rejected: invalid arguments.`)) break;
1576
1671
  continue;
1577
1672
  }
1578
1673
  if (candidateAction.tweetId && observed.byId.has(candidateAction.tweetId)) {
@@ -1581,6 +1676,9 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1581
1676
  candidateAction.targetHandle = `@${author}`;
1582
1677
  }
1583
1678
  }
1679
+ const actionTargetHandle = normalizeHandle2(
1680
+ candidateAction.targetHandle ?? candidateAction.handle ?? (candidateAction.tweetId ? observed.byId.get(candidateAction.tweetId)?.authorHandle : void 0)
1681
+ );
1584
1682
  if (candidateAction.action === "skip") {
1585
1683
  const reason = candidateAction.reason ?? candidateAction.reasoning ?? "planner skip";
1586
1684
  policyFeedback.push(`Planner chose skip: ${reason}`);
@@ -1589,10 +1687,76 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1589
1687
  tool: decision.tool,
1590
1688
  summary: reason
1591
1689
  });
1592
- staleSteps += 1;
1593
- if (staleSteps >= 4) break;
1690
+ if (markNoProgress()) break;
1691
+ continue;
1692
+ }
1693
+ if (constraints.noOriginalPosts && (candidateAction.action === "post" || candidateAction.action === "schedule")) {
1694
+ const reason = "Original posts are disabled by persona constraints.";
1695
+ policyFeedback.push(reason);
1696
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1697
+ if (markNoProgress()) break;
1594
1698
  continue;
1595
1699
  }
1700
+ if (strictReplyHandles.size > 0) {
1701
+ if (candidateAction.action !== "reply") {
1702
+ const reason = `Only reply actions are allowed for this persona target: @${[...strictReplyHandles].join(", @")}.`;
1703
+ policyFeedback.push(reason);
1704
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1705
+ if (markNoProgress()) break;
1706
+ continue;
1707
+ }
1708
+ if (!actionTargetHandle || !strictReplyHandles.has(actionTargetHandle)) {
1709
+ const reason = `Reply target must be one of @${[...strictReplyHandles].join(", @")}.`;
1710
+ policyFeedback.push(reason);
1711
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1712
+ if (markNoProgress()) break;
1713
+ continue;
1714
+ }
1715
+ }
1716
+ if (strictInteractHandles.size > 0 && ["reply", "like", "retweet", "follow"].includes(candidateAction.action)) {
1717
+ if (!actionTargetHandle || !strictInteractHandles.has(actionTargetHandle)) {
1718
+ const reason = `Interaction target must be one of @${[...strictInteractHandles].join(", @")}.`;
1719
+ policyFeedback.push(reason);
1720
+ logger.info(`Policy blocked action ${candidateAction.action}: ${reason}`);
1721
+ if (markNoProgress()) break;
1722
+ continue;
1723
+ }
1724
+ }
1725
+ if (candidateAction.action === "reply" && candidateAction.tweetId) {
1726
+ if (blockedReplyTargets.has(candidateAction.tweetId)) {
1727
+ const reason = `Reply target ${candidateAction.tweetId} is temporarily blocked this heartbeat. Pick a different tweet.`;
1728
+ policyFeedback.push(reason);
1729
+ logger.info(`Planner loop guard: ${reason}`);
1730
+ if (markNoProgress()) break;
1731
+ continue;
1732
+ }
1733
+ if (usedReplyTargets.has(candidateAction.tweetId)) {
1734
+ const reason = `Reply target ${candidateAction.tweetId} already used this heartbeat. Pick a different tweet.`;
1735
+ policyFeedback.push(reason);
1736
+ logger.info(`Planner loop guard: ${reason}`);
1737
+ blockedReplyTargets.add(candidateAction.tweetId);
1738
+ if (markNoProgress()) break;
1739
+ continue;
1740
+ }
1741
+ if (recentReplyTargets.has(candidateAction.tweetId)) {
1742
+ const reason = `Reply target ${candidateAction.tweetId} is in cooldown from recent history. Pick a fresher target.`;
1743
+ policyFeedback.push(reason);
1744
+ logger.info(`Planner loop guard: ${reason}`);
1745
+ blockedReplyTargets.add(candidateAction.tweetId);
1746
+ if (markNoProgress()) break;
1747
+ continue;
1748
+ }
1749
+ }
1750
+ if ((candidateAction.action === "reply" || candidateAction.action === "post") && candidateAction.content) {
1751
+ const seenDrafts = [...writtenThisHeartbeat, ...recentWrittenSamples].slice(0, 18);
1752
+ if (nearDuplicateDraft(candidateAction.content, seenDrafts)) {
1753
+ const reason = "Draft is too similar to recent writing. Try a new angle before posting.";
1754
+ policyFeedback.push(reason);
1755
+ logger.info(`Planner loop guard: ${reason}`);
1756
+ if (markNoProgress()) break;
1757
+ continue;
1758
+ }
1759
+ }
1596
1760
  attemptedActions += 1;
1597
1761
  let policy = evaluateActionPolicy({
1598
1762
  action: candidateAction,
@@ -1634,9 +1798,24 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1634
1798
  }
1635
1799
  if (!policy.allowed) {
1636
1800
  const reason = policy.reason ?? "Policy rejected action";
1801
+ if (shouldHardBlockPolicyReason(reason)) {
1802
+ policyFeedback.push(`Policy blocked action: ${reason}`);
1803
+ logger.info(`Policy blocked ${candidateAction.action}: ${reason}`);
1804
+ if (markNoProgress()) break;
1805
+ continue;
1806
+ }
1637
1807
  policyFeedback.push(`Policy advisory (not blocking): ${reason}`);
1638
1808
  logger.info(`Policy advisory for ${candidateAction.action}: ${reason}`);
1639
1809
  }
1810
+ if (candidateAction.action === "reply" && candidateAction.tweetId) {
1811
+ usedReplyTargets.add(candidateAction.tweetId);
1812
+ }
1813
+ if ((candidateAction.action === "reply" || candidateAction.action === "post") && candidateAction.content) {
1814
+ writtenThisHeartbeat.unshift(candidateAction.content.trim());
1815
+ if (writtenThisHeartbeat.length > 16) {
1816
+ writtenThisHeartbeat.length = 16;
1817
+ }
1818
+ }
1640
1819
  const result = await executeAction(candidateAction);
1641
1820
  actions.push(candidateAction);
1642
1821
  results.push(result);
@@ -1667,6 +1846,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1667
1846
  if (candidateAction.action === "reply" && /duplicate content/i.test(err)) {
1668
1847
  const reason = "Reply failed with duplicate-content error. Planner should choose a different wording/target.";
1669
1848
  policyFeedback.push(reason);
1849
+ if (candidateAction.tweetId) blockedReplyTargets.add(candidateAction.tweetId);
1670
1850
  logger.info(`Policy adjustment: ${reason}`);
1671
1851
  }
1672
1852
  if (candidateAction.action === "post" && /duplicate content/i.test(err)) {
@@ -1675,11 +1855,7 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1675
1855
  logger.info(`Policy adjustment: ${reason}`);
1676
1856
  }
1677
1857
  }
1678
- if (!stepProgress) {
1679
- staleSteps += 1;
1680
- }
1681
- if (staleSteps >= 5) {
1682
- logger.info("Tool loop stopped after repeated no-progress steps.");
1858
+ if (!stepProgress && markNoProgress()) {
1683
1859
  break;
1684
1860
  }
1685
1861
  }
@@ -1696,4 +1872,4 @@ async function runAutonomyCycle(maxActions, heartbeatCount = 0) {
1696
1872
  export {
1697
1873
  runAutonomyCycle
1698
1874
  };
1699
- //# sourceMappingURL=chunk-TTDQZI5W.js.map
1875
+ //# sourceMappingURL=chunk-245JDXK4.js.map