From 8b00e5917eeb9e6a9abfbfe8469146aeb17d4407 Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Thu, 5 Mar 2026 17:55:17 -0800 Subject: [PATCH] feat: implement persistent inbox processing via stateful action tracking and remove sync limit --- src/void_tools/cli.py | 83 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 70 insertions(+), 13 deletions(-) diff --git a/src/void_tools/cli.py b/src/void_tools/cli.py index 457a5e7..c9b52b8 100644 --- a/src/void_tools/cli.py +++ b/src/void_tools/cli.py @@ -88,11 +88,37 @@ def cmd_sync(args): memfs_users_dir = os.path.expanduser(f"~/.letta/agents/{agent_id}/memory/users") try: - inbox = [] - mapping = {} + # Load existing inbox if it exists so we can accumulate + existing_inbox = [] + if os.path.exists("inbox.yaml"): + with open("inbox.yaml", "r") as f: + existing_inbox = yaml.safe_load(f) or [] + + existing_mapping = {} + if os.path.exists(".inbox_mapping.json"): + with open(".inbox_mapping.json", "r") as f: + existing_mapping = json.load(f) + + inbox = existing_inbox.copy() + mapping = existing_mapping.copy() + + # Determine highest existing local_id index to prevent collisions + start_index = 0 + for key in mapping.keys(): + if key.startswith("notif_"): + try: + idx = int(key.split("_")[1]) + if idx >= start_index: + start_index = idx + 1 + except: + pass + cursor = None stop_pagination = False - latest_seen_timestamp = "" + latest_seen_timestamp = mapping.get("_sync_timestamp", "") + + # Track URIs already in inbox to prevent duplicates + existing_uris = set(mapping.values()) while not stop_pagination: params = {'limit': 50} @@ -107,16 +133,11 @@ def cmd_sync(args): for notif in response.notifications: # Filter out passive engagement (likes and non-quote reposts) if notif.reason in ['like', 'repost']: - # We still update the latest seen timestamp to not break the sync marker if not latest_seen_timestamp or notif.indexed_at > latest_seen_timestamp: latest_seen_timestamp = notif.indexed_at continue - if args.unread_only and notif.is_read: - stop_pagination = True - break - - if not args.unread_only and len(inbox) >= 100: + if notif.is_read: stop_pagination = True break @@ -124,7 +145,12 @@ def cmd_sync(args): if not latest_seen_timestamp or notif.indexed_at > latest_seen_timestamp: latest_seen_timestamp = notif.indexed_at - local_id = f"notif_{len(inbox)}" + # Skip if already in inbox + if notif.uri in existing_uris: + continue + + local_id = f"notif_{start_index}" + start_index += 1 handle = notif.author.handle item = { "id": local_id, @@ -229,6 +255,10 @@ def cmd_sync(args): # motivation: "commenting" # Optional: commenting, highlighting, describing # quote: "exact text to anchor to" # Optional # +# 4. Ignore (Remove from inbox without replying) +# - action: ignore +# target_id: "notif_2" +# """ template = { "config": {"mark_seen": False}, @@ -262,8 +292,18 @@ def cmd_dispatch(args): client = get_client() actions = outbox.get("actions", []) + processed_targets = set() + for act in actions: action_type = act.get("action") + target_id = act.get("target_id") + + if action_type == "ignore": + if target_id: + processed_targets.add(target_id) + print(f"Ignoring {target_id}.") + continue + posts = act.get("posts", []) if action_type == "post": @@ -299,6 +339,7 @@ def cmd_dispatch(args): print(f"Error: target_id {target_id} not found in mapping. Skipping.") continue + processed_targets.add(target_id) target_uri = mapping[target_id] print(f"Dispatching reply to {target_uri}...") @@ -337,6 +378,8 @@ def cmd_dispatch(args): if target_id not in mapping: print(f"Error: target_id {target_id} not found in mapping. Skipping annotation.") continue + + processed_targets.add(target_id) if not text: print("Error: annotation requires 'text' field. Skipping.") continue @@ -400,6 +443,22 @@ def cmd_dispatch(args): except Exception as e: print(f"Error marking seen: {e}") + # After processing all actions, remove processed items from inbox.yaml + if processed_targets and os.path.exists("inbox.yaml"): + try: + with open("inbox.yaml", "r") as f: + current_inbox = yaml.safe_load(f) or [] + + # Keep items whose ID is not in processed_targets + new_inbox = [item for item in current_inbox if item.get("id") not in processed_targets] + + with open("inbox.yaml", "w") as f: + yaml.dump(new_inbox, f, sort_keys=False, default_flow_style=False) + + print(f"Removed {len(processed_targets)} processed items from inbox.") + except Exception as e: + print(f"Error updating inbox.yaml: {e}") + # Always delete the outbox file after successful dispatch to prevent stale state loops try: os.remove(args.file) @@ -583,9 +642,7 @@ def main(): notif_parser.add_argument("--mark-seen", action="store_true", help="Mark fetched notifications as seen") # Sync - sync_parser = subparsers.add_parser("sync", help="Sync unread notifications to inbox.yaml") - sync_parser.add_argument("--all", dest="unread_only", action="store_false", help="Fetch read notifications as well") - sync_parser.set_defaults(unread_only=True) + sync_parser = subparsers.add_parser("sync", help="Sync unread notifications to inbox.yaml (accumulates unread items)") # Post post_parser = subparsers.add_parser("post", help="Create a post or reply") -- 2.51.2