@m13v/s4l 1.7.5-rc.44 → 1.7.5-rc.45

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.
package/mcp/dist/index.js CHANGED
@@ -1011,6 +1011,27 @@ async function postApproved(batchId, plan) {
1011
1011
  plan = fresh;
1012
1012
  }
1013
1013
  }
1014
+ // Arm the cross-instance posting flag HERE, before the Reddit drain, not just
1015
+ // before the Twitter phase below. Root cause (found 2026-07-16): this flag
1016
+ // used to get set only right before ensureTwitterBrowserForPost(), AFTER the
1017
+ // whole Reddit drain loop above (one runPython("post_reddit.py") call per
1018
+ // approved Reddit card, each with real CDP/browser overhead — a batch of a
1019
+ // handful of cards routinely runs minutes). For that entire window this
1020
+ // instance's isPeerDrainActive() reported false, so a SECOND MCP instance's
1021
+ // postApproved() (e.g. a fresh one-shot worker boot running its own startup
1022
+ // backlog drain) could pass the SAME gate check above and start its own
1023
+ // drain concurrently. Both eventually reached their own Twitter phase and
1024
+ // both spawned scripts/twitter_post_plan.py with S4L_LOCK_ROLE=post — the
1025
+ // ONLY code path that ever sets that role. Two live "post" role holders
1026
+ // can't preempt each other (browser_mutex.py case 6: correct, preempting a
1027
+ // peer mid-post risks a double-post), so one always waited 45s and gave up
1028
+ // — the "Twitter browser locked by session python:<pid> ... waited 45s,
1029
+ // giving up" failures that silently killed 11 real approved drafts over
1030
+ // 2026-07-15/16. Arming the flag for the FULL function closes that window:
1031
+ // a peer's gate check now blocks for the entire drain, Reddit included, so
1032
+ // only one instance ever reaches the Twitter phase at a time.
1033
+ postingActive = true;
1034
+ startPostingFlagHeartbeat();
1014
1035
  // Post every card the user APPROVED that hasn't already landed or been ruled out.
1015
1036
  // `approved` is now a DURABLE decision (sticky, never cleared by a later call), so
1016
1037
  // filtering out posted/terminal here makes this idempotent: re-running it only
@@ -1029,8 +1050,11 @@ async function postApproved(batchId, plan) {
1029
1050
  delete c.discard_reason;
1030
1051
  }
1031
1052
  }
1032
- if (approved.length === 0)
1053
+ if (approved.length === 0) {
1054
+ postingActive = false;
1055
+ stopPostingFlagHeartbeat();
1033
1056
  return { attempted: 0, exit_code: 0, summary: "nothing approved" };
1057
+ }
1034
1058
  // ---- Reddit cards (2026-07-14): drain them FIRST, independently ----------
1035
1059
  // A reddit card carries the verbatim draft decision (reddit_decision) plus
1036
1060
  // plan metadata (reddit_plan_meta), so approval reconstructs a one-decision
@@ -1204,7 +1228,10 @@ async function postApproved(batchId, plan) {
1204
1228
  }
1205
1229
  if (approvedTwitter.length === 0) {
1206
1230
  // All-reddit batch: persist the stamps and return without touching the
1207
- // twitter preflight/lock path.
1231
+ // twitter preflight/lock path. Must clear the flag armed above ourselves —
1232
+ // this branch never reaches the Twitter phase's scheduleShellLockRelease().
1233
+ postingActive = false;
1234
+ stopPostingFlagHeartbeat();
1208
1235
  if (redditSkippedPeerDrain) {
1209
1236
  return {
1210
1237
  attempted: 0,
@@ -1230,6 +1257,8 @@ async function postApproved(batchId, plan) {
1230
1257
  if (!readConfiguredTwitterHandle())
1231
1258
  await ensurePostingHandle();
1232
1259
  if (!readConfiguredTwitterHandle()) {
1260
+ postingActive = false;
1261
+ stopPostingFlagHeartbeat();
1233
1262
  return {
1234
1263
  attempted: 0,
1235
1264
  exit_code: 0,
@@ -2863,7 +2892,22 @@ tool("post_drafts", {
2863
2892
  warnings,
2864
2893
  });
2865
2894
  }
2866
- const result = await postApproved(batch_id, plan);
2895
+ // postApproved now arms the cross-instance posting flag before the Reddit
2896
+ // drain (see its own comment), not just before the Twitter phase. Every
2897
+ // op inside that drain is already individually best-effort try/caught, so
2898
+ // this is belt-and-suspenders: if something still throws past all of
2899
+ // that, clear the flag before it escapes rather than leave this MCP
2900
+ // instance's posting permanently gated behind a flag nothing will ever
2901
+ // reset.
2902
+ let result;
2903
+ try {
2904
+ result = await postApproved(batch_id, plan);
2905
+ }
2906
+ catch (e) {
2907
+ postingActive = false;
2908
+ stopPostingFlagHeartbeat();
2909
+ throw e;
2910
+ }
2867
2911
  // Report the REAL posted count from the pipeline, not the approved count.
2868
2912
  // A run can approve N yet land 0 (browser/session failure); reporting
2869
2913
  // approve.size here told the agent "posted: N" on a total failure.
@@ -5658,6 +5702,10 @@ async function drainApprovedBacklog() {
5658
5702
  }
5659
5703
  catch (e) {
5660
5704
  console.error("[post] drainApprovedBacklog error:", e?.message || e);
5705
+ // Same reasoning as the other postApproved call site: don't let an
5706
+ // escaped exception leave the cross-instance posting flag stuck true.
5707
+ postingActive = false;
5708
+ stopPostingFlagHeartbeat();
5661
5709
  }
5662
5710
  }
5663
5711
  async function main() {
@@ -1,4 +1,4 @@
1
1
  {
2
- "version": "1.7.5-rc.44",
3
- "installedAt": "2026-07-16T18:49:09.291Z"
2
+ "version": "1.7.5-rc.45",
3
+ "installedAt": "2026-07-16T19:01:18.006Z"
4
4
  }
package/mcp/manifest.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "dxt_version": "0.1",
3
3
  "name": "social-autoposter",
4
4
  "display_name": "S4L",
5
- "version": "1.7.5-rc.44",
5
+ "version": "1.7.5-rc.45",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "## **⚠️ The disclaimer above is generic Claude boilerplate.** Anthropic shows the same warning on every plugin regardless of what it does; any plugin has the same level of access as any app you download from the internet.\n\nS4L is an open source product developed by Mediar.ai Incorporated, a VC-backed San Francisco-based startup.\n\nTo get started:\n\n1\\. Copy this prompt: **Set me up on S4L plugin end to end**\n\n2\\. Quit with CMD+Q, reopen Claude, paste into a new chat.\n\nWhat happens next:\n\n* About every 5 minutes S4L scans X for posts that match your topics and drafts replies in your voice.\n* Drafts show up as review cards, usually the first within a few minutes. Nothing is posted automatically; you approve each one.\n* Posting autopilot stays off until you explicitly turn it on.",
8
8
  "author": {
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l-mcp",
3
- "version": "1.7.5-rc.44",
3
+ "version": "1.7.5-rc.45",
4
4
  "private": true,
5
5
  "description": "Desktop MCP client for social-autoposter (X/Twitter rail): manual draft/review/approve loop, autopilot control, and stats. Thin wrapper over the existing pipeline scripts.",
6
6
  "license": "MIT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/s4l",
3
- "version": "1.7.5-rc.44",
3
+ "version": "1.7.5-rc.45",
4
4
  "description": "Automated social posting pipeline for Reddit, X/Twitter, LinkedIn, and Moltbook. Install as a Claude Code agent skill.",
5
5
  "bin": {
6
6
  "social-autoposter": "bin/cli.js",