social-autoposter 1.6.154 → 1.6.156

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,4 +1,4 @@
1
1
  {
2
- "version": "1.6.154",
3
- "installedAt": "2026-06-29T23:14:44.360Z"
2
+ "version": "1.6.156",
3
+ "installedAt": "2026-06-29T23:25:31.893Z"
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.6.154",
5
+ "version": "1.6.156",
6
6
  "description": "Draft, review, approve, and autopilot X/Twitter posts.",
7
7
  "long_description": "The disclaimer above is generic Claude boilerplate. S4L 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 end to end**\n\n2\\. Quit fully with CMD+Q, restart Claude, and paste the prompt into a new chat.",
8
8
  "author": {
@@ -22,6 +22,7 @@ import os
22
22
  import subprocess
23
23
  import sys
24
24
  import threading
25
+ import time
25
26
  import urllib.request
26
27
  from pathlib import Path
27
28
 
@@ -195,32 +196,58 @@ def loopback_tool(name: str, args=None, timeout: float = 20.0):
195
196
 
196
197
 
197
198
  # ---- the snapshot the menu bar renders ------------------------------------
198
- def snapshot():
199
- """Full snapshot computed DIRECTLY from the stateful files via
200
- scripts/snapshot.py the SAME single-source module the MCP shells out to, so
201
- the two surfaces can't diverge. The menu bar no longer depends on the MCP /
202
- Claude being up: there is NO loopback call here, so a restarting or closed
203
- Claude can't freeze or stale the menu (the old tier-1 `loopback_tool` blocked
204
- the UI thread up to 20s and was the freeze).
205
-
206
- Three tiers, in order:
207
- 1. LIVE — compute locally from the files (zero MCP dependency).
208
- 2. SUMMARY the server's last persisted `status-summary.json`, if the local
209
- compute somehow failed.
210
- 3. LEDGER nothing else available: derive the essentials from the onboarding
211
- ledger so progress still shows."""
199
+ # Background snapshot cache. scripts/snapshot.py reads files but may spawn the
200
+ # X-status subprocess (setup_twitter_auth.py -> CDP to Chrome), which must NEVER
201
+ # run on the menu bar's UI thread a hung Chrome would freeze the menu. So a
202
+ # daemon thread recomputes and snapshot() returns the last cached value INSTANTLY.
203
+ _snap_cache = {"val": None, "at": 0.0}
204
+ _snap_lock = threading.Lock()
205
+ _snap_refreshing = [False]
206
+
207
+
208
+ def _compute_snapshot_full():
209
+ repo = os.environ.get("SAPS_REPO_DIR") or str(Path.home() / "social-autoposter")
210
+ scripts = os.path.join(repo, "scripts")
211
+ if scripts not in sys.path:
212
+ sys.path.insert(0, scripts)
213
+ import snapshot as _snapshot_mod # scripts/snapshot.py
214
+ return _snapshot_mod.compute()
215
+
216
+
217
+ def _refresh_snapshot_bg():
212
218
  try:
213
- repo = os.environ.get("SAPS_REPO_DIR") or str(Path.home() / "social-autoposter")
214
- scripts = os.path.join(repo, "scripts")
215
- if scripts not in sys.path:
216
- sys.path.insert(0, scripts)
217
- import snapshot as _snapshot_mod # scripts/snapshot.py
218
- snap = _snapshot_mod.compute()
219
+ snap = _compute_snapshot_full()
219
220
  if isinstance(snap, dict) and "projects_total" in snap:
220
- snap["_live"] = True
221
- return snap
221
+ with _snap_lock:
222
+ _snap_cache["val"] = snap
223
+ _snap_cache["at"] = time.time()
222
224
  except Exception:
223
225
  pass
226
+ finally:
227
+ _snap_refreshing[0] = False
228
+
229
+
230
+ def snapshot():
231
+ """Full snapshot computed DIRECTLY from the stateful files via
232
+ scripts/snapshot.py — the SAME single-source module the MCP shells out to, so
233
+ the two surfaces can't diverge. NO loopback / MCP dependency, so a restarting
234
+ or closed Claude can't freeze or stale the menu (the old tier-1 `loopback_tool`
235
+ blocked the UI thread up to 20s and was the freeze). The heavy compute runs on
236
+ a BACKGROUND thread; this returns the last cached result instantly.
237
+
238
+ Tiers: (1) the background-computed local snapshot; (2) the server's last
239
+ persisted `status-summary.json`; (3) the onboarding ledger."""
240
+ now = time.time()
241
+ with _snap_lock:
242
+ cached = _snap_cache["val"]
243
+ age = now - _snap_cache["at"]
244
+ if (cached is None or age > 4.0) and not _snap_refreshing[0]:
245
+ _snap_refreshing[0] = True
246
+ threading.Thread(target=_refresh_snapshot_bg, daemon=True).start()
247
+ if cached is not None:
248
+ out = dict(cached)
249
+ out["_live"] = True
250
+ return out
224
251
  summ = read_json("status-summary.json")
225
252
  if isinstance(summ, dict) and "projects_total" in summ:
226
253
  summ["_live"] = False
package/mcp/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@m13v/social-autoposter-mcp",
3
- "version": "1.6.154",
3
+ "version": "1.6.156",
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": "social-autoposter",
3
- "version": "1.6.154",
3
+ "version": "1.6.156",
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"
@@ -249,14 +249,29 @@ def main() -> int:
249
249
  dry = "--dry-run" in sys.argv
250
250
  max_age = _env_int("SAPS_REAPER_MAX_AGE_SEC", DEFAULT_MAX_AGE_SEC)
251
251
  # (1) Queue-correlated reaping knobs.
252
- # grace: how long an UNCLAIMED session may live before it's reapable. Raised
253
- # 90 -> 300 on 2026-06-29: at 90s the reaper was SIGTERMing actively-DRAFTING
254
- # worker sessions (a draft legitimately runs minutes), which presented as the
255
- # mysterious "~120s code-143 kill". Claim-holders are now spared outright via
256
- # running_claim_pids() regardless of grace, so this only governs idle/leaked
257
- # sessions; 5 min is a comfortable backstop for them and the count-cap still
258
- # bounds memory if the queue signal is ever wrong.
259
- grace = _env_int("SAPS_REAPER_GRACE_SEC", 300) # idle/leaked-session backstop
252
+ # grace: how long an UNCLAIMED session may live before its AGE makes it reapable.
253
+ #
254
+ # 2026-06-29, second pass: this used to default to 90s, and the queue-readable
255
+ # branch below used it as the ONLY age gate -- which meant a separate, short,
256
+ # activity-BLIND timer governed normal operation and silently overrode the 35-min
257
+ # `max_age` ceiling that the whole top-of-file rationale is built around (producer
258
+ # deadline 1800s + margin). An actively-DRAFTING session ages out of the
259
+ # "inflight+margin newest" window after ~2 min as fresh empty workers spawn on top
260
+ # of it, then got SIGTERMed at 90s mid-draft -> the mysterious "~120s code-143
261
+ # kill". Raising it to 300s only moved the cliff.
262
+ #
263
+ # The fix is to stop having two timers. There is now ONE overall age ceiling
264
+ # (`max_age`, 35 min = producer deadline + margin): past it the producer has
265
+ # already discarded the worker's result, so the session is provably useless and
266
+ # safe to reap whether or not it ever claimed. `grace` therefore defaults to
267
+ # `max_age` -- the queue-readable branch is no longer allowed to preempt a session
268
+ # earlier than the producer's own deadline. What keeps MEMORY bounded instead of a
269
+ # short timer is (a) claim-holders are spared outright via running_claim_pids() and
270
+ # (b) the count-cap (max_group) reaps the oldest-beyond-N by COUNT, regardless of
271
+ # age, and never touches a claim-holder. Override only to deliberately re-introduce
272
+ # an earlier age gate (not recommended); it can never exceed max_age in effect.
273
+ grace = _env_int("SAPS_REAPER_GRACE_SEC", DEFAULT_MAX_AGE_SEC) # = max_age: one ceiling
274
+ grace = min(grace, max_age) # an override may only shorten, never outlive the ceiling
260
275
  keep_margin = _env_int("SAPS_REAPER_KEEP_MARGIN", 1) # extra newest spared beyond busy set
261
276
  # (2) Count-cap backstop: never let one uuid group hold more than this many live
262
277
  # workers, regardless of queue state. 0 disables. The default rarely fires once
@@ -21,15 +21,32 @@ HB_PID="" # scan-phase heartbeat (started below); torn down by the EXIT trap
21
21
  # the cycle's exit code.
22
22
  trap 'kill "$HB_PID" 2>/dev/null || true; rm -f "$OUT"; "$PY" "$REPO_DIR/scripts/saps_activity.py" clear 2>/dev/null || true' EXIT
23
23
 
24
- # Narrate the scan phase. The CDP scan runs inside the (locked) run-twitter-cycle.sh
25
- # which has no activity writer; this covers that window until the queue provider
26
- # flips the label to "finding threads"/"drafting replies" on its first claude call.
24
+ # Narrate the scan phase, GRANULARLY. The CDP scan runs inside the (locked)
25
+ # run-twitter-cycle.sh which has no activity writer; this covers that window until
26
+ # the queue provider flips the label to "finding threads"/"drafting replies".
27
+ # Instead of a frozen "scanning X for threads" for the whole multi-minute scan,
28
+ # each heartbeat recomputes elapsed and scrapes THIS cycle's own stdout ($OUT, the
29
+ # tee target below) for live progress — queries run, and candidates found once
30
+ # Phase 1 reports them — so the menu bar actually moves. Reads $OUT only; never
31
+ # touches the locked cycle. heartbeat() re-stamps ONLY while the state is still
32
+ # "scanning", so once the provider advances the phase it goes quiet (no flicker).
27
33
  "$PY" "$REPO_DIR/scripts/saps_activity.py" write scanning "scanning X for threads" 2>/dev/null || true
28
- # Keep "scanning" fresh against the menu bar's staleness TTL for the whole scan
29
- # (which can run minutes with no other writer). The heartbeat re-stamps ONLY while
30
- # the state is still "scanning", so once the queue provider advances the phase to
31
- # "finding threads"/"drafting replies" it goes quiet and never fights that writer.
32
- ( while true; do sleep 30; "$PY" "$REPO_DIR/scripts/saps_activity.py" heartbeat scanning "scanning X for threads" 2>/dev/null || true; done ) &
34
+ SCAN_T0=$(date +%s)
35
+ (
36
+ while true; do
37
+ sleep 20
38
+ _el=$(( $(date +%s) - SCAN_T0 ))
39
+ if [ "$_el" -lt 60 ]; then _dur="${_el}s"; else _dur="$(( _el / 60 ))m"; fi
40
+ _q=$(grep -c "kept=" "$OUT" 2>/dev/null || true); _q=${_q:-0}
41
+ _found=$(grep -oE "Batch has [0-9]+" "$OUT" 2>/dev/null | tail -1 | grep -oE "[0-9]+" | tail -1 || true)
42
+ if [ -n "$_found" ]; then
43
+ _lbl="scanning X for threads (${_dur} · ${_q} queries, ${_found} found)"
44
+ else
45
+ _lbl="scanning X for threads (${_dur} · ${_q} queries)"
46
+ fi
47
+ "$PY" "$REPO_DIR/scripts/saps_activity.py" heartbeat scanning "$_lbl" 2>/dev/null || true
48
+ done
49
+ ) &
33
50
  HB_PID=$!
34
51
 
35
52
  # Engagement mode (2026-06-26). The menu-bar toggle writes mode.json; this reads