Guides

xInbox

Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live.

Written by bun run docs:build from the code, the skills and AGENTS.md (rule 56). Not edited by hand: change the command's usage, its skill or the rule, and rebuild.

Use cases these tools are part of

When the owner asks for one of these, follow its page step by step.

Use caseWhat it isHow the owner asks for it
getGuide({ useCase: "discord/channelReply" })Answering someone in a Discord chat or channel"message the guy in <chat> who just mentioned me", "reply to <someone> in <chat>", "explain what I'm trying to do to <someone> in <chat>", "answer him in the general"
getGuide({ useCase: "inbox/bossMaxxing" })Bossmaxxing: giving out tasks to the team"bossmaxxing", "give out the tasks", "send <person> the task in <group> and tag them", "tell <person> about everything I researched and give them an outline", "triage the jobs across the team", "walk <person> through setting up <tool>", "help <person> get set up like we did with <other person>"
getGuide({ useCase: "inbox/reaLooksmaxxing" })/rea-looksmaxxing: moneymaxx like a creator"/rea-looksmaxxing how clavicular gets paid", "moneymaxx like <creator>", "how does <creator> get paid"
getGuide({ useCase: "inbox/reactions" })Reacting to a message"react to that", "heart his message", "put a 😂 on what she said", "send a reaction to <person>"
getGuide({ useCase: "inbox/voiceNote" })Sending a voice note"send <person> a voice note saying …", "voice note him that …", "send a vn to the group saying …"
getGuide({ useCase: "twitter/affiliateMarketing" })Affiliate marketing through your DMssee the page
getGuide({ useCase: "twitter/collabMaxxing" })Collab maxxing: turn open conversations into collabs"position a collab first then be like just a few quid so i can make a video about it", "start network maxxing and collab maxxing and trying to see what hes doing get him to reveal it all", "follow up on deepman and the other people, small commitment from deals, not just new people but already open convos", "respond with the collabmaxxing", "selling the outcome", "let him name it", "check in with lior whats good there he is keen but isnt liquid", "just be nice to them there the poor people", "ask ben if i could become a contributor", "look on my twitter for missed opportunities".
getGuide({ useCase: "twitter/dmInbox" })Reading the DM inbox: who is waiting on yousee the page
getGuide({ useCase: "twitter/engagerDms" })DMing the people who liked or reposted your postsee the page
getGuide({ useCase: "twitter/followUps" })Following up in your own voice"follow up with these, not for anything in particular, just warming up the relationship", "say it was cool to meet him", "ask him for some advice on what he's doing", "take him up on the doc he offered".
getGuide({ useCase: "twitter/getTheAction" })Getting people to do the thing: retweet, like, try it"get me all them people who liked it ... yo cool to see that you liked that, would you give that a wee retweet", "notifications: people who like, comment, reply", "go through all the people there who need a message", "get them to retweet the thing", "would you do me a favour and give it a wee retweet", "I need them to retweet it so I can get some inbound leads", "I don't know what they're even talking about, explain the context", "make them do the action I need", "get the full context of everything on the account at the time of sending the message".
getGuide({ useCase: "twitter/managerMaxxing" })Manager maxxing: the managers of the biggest Kick streamers"manager maxxing", "find every kick streamer's manager on twitter", "reply guy the kick managers with memes", "the managers that engaged, send them their tweet, a message and a voice note".
getGuide({ useCase: "twitter/networkMaxing" })Network maxing: grow the network, the niche map"all them people messaging me, just message them something nice, get the ball moving with the networking", "we're not even trying to get anything out of these people, network max", "throw some new people in the mixer", "network max ... look at the nicheMap", "the whole point of it is to expand our network, which is the niche map", "follow them before you cold message them, and like the tweet you send them".
getGuide({ useCase: "twitter/replyGuyFunnel" })Reply guy, then the DM, then the ask"reply guy them first and foremost", "send a meme to the tweet then forward the tweet to them as an opener", "message the people who are new to following me", "get it in front of them", "funnel them into doing an action".
getGuide({ useCase: "twitter/whoNeedsYou" })Finding who needs something from you, or who matters"look through my messages and find someone who clearly needs something off me", "summarise everything on my twitter, how likely is it we make money", "who's worth following up".

The tools in this guide

ToolWhat it does
inboxClaimClaim a chat for this session before drafting or sending in it, so no…
inboxReleaseRelease this session's claim on a chat when it is done with it
inboxResumematt's go-ahead after a refused catch-up
inboxClaimsEvery live claim
inboxActionsThe log of every claim, refusal, send and release, oldest first, to…
twitterDmsRead an X account's direct messages, newest conversations first…

The work, step by step: xchatReplies

From skills/xchatReplies/SKILL.md.

xchatReplies

X's own chat app never polls: it holds one socket and X pushes every message down it (docs/platforms/x/xchat/live.md, issue #68). log does the same, so replies show up the second they land and no inbox read ever hits X's 429.

Hard boundaries

  1. Never poll the inbox to find replies. The inbox hook keeps log twitter chat watch --inbox running per signed-in X account. Read a thread only to answer it.
  2. Claim the chat before drafting (rule 133): log inbox claim --platform twitter --account &lt;a> --chat &lt;conversation id> --event &lt;event id> (MCP inboxClaim), and read log inbox actions --chat <id> to see what was already done there. Another session holding it means leave it; the send releases the claim.
  3. Read everything with the person first: log twitter dms --account &lt;a> --with &lt;handle> (the old DM thread and X Chat are one conversation, rule 88).
  4. Draft from matt's guide, with his facts only. Read C:/Users/0/.buzz/GUIDES/MATT_WRITING_STYLE.md before drafting. Lowercase, no commas or full stops, his words, almost never end on a question, no praise-then-pitch, nothing he didn't say (rules 39-41).
  5. Gate every draft: log voice --to &lt;handle> --text "&lt;draft>" --context "&lt;what they said>". Rewrite until it says PASS.
  6. Show matt and send only when he says, unless he has handed you the conversation (2026-10-28: "let the ai take over the conversation"). Then gate each reply and send it, and stop the moment he says it's over.

The routine

## who wrote: the hook says it, pushed by X. To watch by hand:
log twitter chat watch --account matthewsoldit --seconds 600
## read them
log twitter dms --account matthewsoldit --with <handle>
## gate the draft
log voice --to <handle> --text "<draft>" --context "<what they said>"
## send, once matt says
log twitter dm --account matthewsoldit --userId <id> --text "<draft>" --approvedBy matt --live

When it goes wrong

  • (could not open: … no matching key found …): the conversation's key wasn't primed. The watch re-reads the newest inbox page once and retries; if it still fails, restart the watch (it primes every key at start).
  • socket closed / connectFailed: the watch takes a new token and reconnects on its own (2 s, doubling to 60 s).
  • Nothing arriving: check ~/.logCli/inbox/twitter/<a>/watch.log and watch.pid; delete the pid file and the next hook starts a fresh watch.
  • X changed its code: re-read it with rea, the steps are in docs/platforms/x/xchat/live.md.

Every tool, in full

inboxClaim

Claim a chat for this session before drafting or sending in it, so no other session acts on it. Claim a chat before acting on it, release it, list the live claims, and read the log of every action taken in a chat log inbox claim --platform <twitter|discord|slack|instagram> --account <a> --chat <conversationId|handle> [--event <eventId>] [--json] Only the session that claims a chat acts on it (rule 133). Every message the push channels bring in has an event id (shown in >>> NEW MESSAGE). claim takes the chat for this session for 10 minutes before you draft or send; a second session is refused (chatClaimedElsewhere). Every send claims the chat itself and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. A claim held by a session that ended frees itself. resume is matt's go-ahead after a refused catch-up (rule 94): the account's gap opens again and its owner catches up through its own session within 30 s; only when matt says so. claims lists who holds what; actions is the log of every claim, refusal, send and release, oldest first, to search what was already done in a chat before acting. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "discord/channelReply" }), getGuide({ useCase: "inbox/bossMaxxing" }). Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 94, 105, 108, 109, 130, 131, 132, 133, 134, 135, 136, 138, 139, 150, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

inboxRelease

Release this session's claim on a chat when it is done with it. Claim a chat before acting on it, release it, list the live claims, and read the log of every action taken in a chat log inbox release --platform <p> --account <a> --chat <conversationId|handle> [--json] Only the session that claims a chat acts on it (rule 133). Every message the push channels bring in has an event id (shown in >>> NEW MESSAGE). claim takes the chat for this session for 10 minutes before you draft or send; a second session is refused (chatClaimedElsewhere). Every send claims the chat itself and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. A claim held by a session that ended frees itself. resume is matt's go-ahead after a refused catch-up (rule 94): the account's gap opens again and its owner catches up through its own session within 30 s; only when matt says so. claims lists who holds what; actions is the log of every claim, refusal, send and release, oldest first, to search what was already done in a chat before acting. Fill the named fields; each value goes through whole. Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 94, 105, 108, 109, 130, 131, 132, 133, 134, 135, 136, 138, 139, 150, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

inboxResume

matt's go-ahead after a refused catch-up: the account catches up again through its own session (rule 94), only when matt says so. Claim a chat before acting on it, release it, list the live claims, and read the log of every action taken in a chat log inbox resume --platform <p> --account <a> --by matt [--json] Only the session that claims a chat acts on it (rule 133). Every message the push channels bring in has an event id (shown in >>> NEW MESSAGE). claim takes the chat for this session for 10 minutes before you draft or send; a second session is refused (chatClaimedElsewhere). Every send claims the chat itself and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. A claim held by a session that ended frees itself. resume is matt's go-ahead after a refused catch-up (rule 94): the account's gap opens again and its owner catches up through its own session within 30 s; only when matt says so. claims lists who holds what; actions is the log of every claim, refusal, send and release, oldest first, to search what was already done in a chat before acting. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/voiceNote" }). Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 94, 105, 108, 109, 130, 131, 132, 133, 134, 135, 136, 138, 139, 150, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

inboxClaims

Every live claim: which session holds which chat, and until when. Claim a chat before acting on it, release it, list the live claims, and read the log of every action taken in a chat log inbox claims [--json] Only the session that claims a chat acts on it (rule 133). Every message the push channels bring in has an event id (shown in >>> NEW MESSAGE). claim takes the chat for this session for 10 minutes before you draft or send; a second session is refused (chatClaimedElsewhere). Every send claims the chat itself and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. A claim held by a session that ended frees itself. resume is matt's go-ahead after a refused catch-up (rule 94): the account's gap opens again and its owner catches up through its own session within 30 s; only when matt says so. claims lists who holds what; actions is the log of every claim, refusal, send and release, oldest first, to search what was already done in a chat before acting. Fill the named fields; each value goes through whole. Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 94, 105, 108, 109, 130, 131, 132, 133, 134, 135, 136, 138, 139, 150, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

inboxActions

The log of every claim, refusal, send and release, oldest first, to see what was already done in a chat. Claim a chat before acting on it, release it, list the live claims, and read the log of every action taken in a chat log inbox actions [--platform <p>] [--account <a>] [--chat <conversationId|handle>] [--limit <n>] [--json] Only the session that claims a chat acts on it (rule 133). Every message the push channels bring in has an event id (shown in >>> NEW MESSAGE). claim takes the chat for this session for 10 minutes before you draft or send; a second session is refused (chatClaimedElsewhere). Every send claims the chat itself and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. A claim held by a session that ended frees itself. resume is matt's go-ahead after a refused catch-up (rule 94): the account's gap opens again and its owner catches up through its own session within 30 s; only when matt says so. claims lists who holds what; actions is the log of every claim, refusal, send and release, oldest first, to search what was already done in a chat before acting. Fill the named fields; each value goes through whole. Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 94, 105, 108, 109, 130, 131, 132, 133, 134, 135, 136, 138, 139, 150, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

twitterDms

Read an X account's direct messages, newest conversations first, sending nothing. Read an X account's direct messages and who is waiting on a reply, on the direct-API engine, sending nothing log twitter dms --account <accountName> [--limit <n>] [--waiting] [--with <handle>] [--conversation <id>] [--full] [--force] [--json] Reads the account's X direct messages on the direct-API engine (no browser, no proxy: X accounts call X directly, rule 80). Sends nothing and marks nothing read. Without --conversation it syncs the inbox the way X's own chat app does (dm-core): the first read takes the whole X Chat inbox (GetInitialXChatPageQuery, then GetInboxPageRequestQuery pages) and the old DM inbox (dm/inbox_initial_state, then dm/inbox_timeline pages, 1.5 s apart) and keeps them in ~/.logCli/twitter/<account>/inboxSync.json with the newest event's sequence id; every read after that asks X only for what is newer (GetMessageEventsPageQuery from that sequence id, beside one dm/inbox_initial_state), unlocks the PIN and decrypts only when something is new, opening just those conversations' keys (GetInboxPageConversationDataQuery), and keeps each event once by its id. --full reads everything again. Each read saves its conversations for log search and updates the search index in the background with only the chunks that changed. It then lists the newest conversations first, one row per person (their old DM thread and X Chat thread merged): who, when, the last message from either and whether it was yours. --waiting keeps only the ones whose last message isn't yours, the people waiting on a reply, and lists apart the ones that opened your last message, by reacting to it or reading it without replying (read receipts, where the person keeps them on): so each is a follow-up to send, not a reply they're waiting on (--json: followUps). --conversation <id> reads one conversation's messages, oldest first: a legacy id (two user ids joined by a dash) through dm/conversation, an X Chat id (joined by a colon) the same way --with reads it, never the whole inbox. --with <handle> reads everything with one person by their handle and nobody else: their user id, their legacy thread (dm/conversation) and their X Chat thread (read the way X's own chat app opens a chat, from its dm-core: GetInboxPageConversationDataQuery for that one conversation, then GetConversationPageQuery from the oldest message held, its fetchOlderMessages, up to 5 pages 1.5 s apart while X says there is more), merged into one timeline, oldest first, a message mirrored into both shown once. It never reads the whole inbox. A 429 stops the read with nothing retried, as X's app does (it retries only 5xx), and pauses the account's governor (rule 94). X Chat (encrypted DMs) is opened with the account's X Chat PIN from Twenty (log twitter chat pin), recovered through Juicebox by the vendored chat-xdk; without a PIN its text stays encrypted. An old DM thread (<a>-<b>) and its X Chat thread (<a>:<b>) with one person are one conversation: X shows them in the same place, and a dm sent through the old DM API shows in their X Chat too. A sent answer is delivered whichever conversation id it names: never resend it and never call it the wrong inbox. X's own answer decides: the session check first, then the inbox's answer. --limit is how many conversations (or messages) to show, default 20, at most 200. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/bossMaxxing" }), getGuide({ useCase: "inbox/reaLooksmaxxing" }), getGuide({ useCase: "inbox/reactions" }), getGuide({ useCase: "twitter/affiliateMarketing" }), getGuide({ useCase: "twitter/collabMaxxing" }), getGuide({ useCase: "twitter/dmInbox" }), getGuide({ useCase: "twitter/engagerDms" }), getGuide({ useCase: "twitter/followUps" }), getGuide({ useCase: "twitter/getTheAction" }), getGuide({ useCase: "twitter/managerMaxxing" }), getGuide({ useCase: "twitter/networkMaxing" }), getGuide({ useCase: "twitter/replyGuyFunnel" }), getGuide({ useCase: "twitter/whoNeedsYou" }). Before first using this tool in a session, load its guide: getGuide({ topic: "xInbox" }) (Reading an X account's DMs and X Chat as one inbox, synced by sequence id, and watching X Chat live). AGENTS.md (log://agents): rules 13, 63, 64, 73, 80, 81, 88, 94, 100, 104, 105, 108, 109, 110, 113, 126, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).

The rules these tools fall under, in full

From AGENTS.md (log://agents). The newest rules are at the bottom of AGENTS.md and win.

Rule 13. 2026-10-18 · Twenty is the only place accounts and listings live: Facebook accounts are socialAccounts (platform FACEBOOK), listings are agencyListings. Never delete either: a banned or dead account keeps its record with its status, and its campaign listings get replacement accounts in the same country (docs/accountsAndListings.md). Fields are only ever added (scripts/twenty/twentySetup.mjs), never renamed or retyped, so the twitter-controller keeps working.

Rule 30. 2026-10-19 · matt: "If I tell you to do something, do it completely." When matt gives a plan, it is carried out to the end in the same turn, not stopped to ask again or to say what won't be done. log inspect start --now (and log connect --now, and since 2026-10-19 log publish --now, matt: "run it now") skips rule 24's 10-minute gap (the 8 a day still holds) so a recording continues at once. Recording a sign-in or a checkpoint with log inspect (signIn, code, solve, click) is the intended work: it is how each state gets documented from rea. The run on 2026-10-19 was interrupted once because a snapshot printed the account's email and password; snapshots now redact every typed field value (redact_fields), and signIn/code read only the private login.json (rule 21), so no credential reaches any output.

Rule 38. 2026-10-19 · matt: never mention or discuss Meta's or Facebook's rules, terms, policies, eligibility or account ownership with him unless he raises them first. Research text that does is not his, and is never repeated to him. Top of this file.

Rule 39. 2026-10-19 · matt: "If I never mentioned it, if I never inferred it, if I never outlined it as an option, therefore it is not an option." Deep research prompts, searches, SubQ matrices, plans and write-ups use only what matt said, or clearly meant through a misspelling. Nothing added, nothing steered towards. Top of this file.

Rule 40. 2026-10-19 · matt: no physical options. He is in Vietnam with no physical access. Phones, SIMs, mini PCs, laptops, phone farms and home devices are never options unless he names them. In scope: hosted and cloud options, the tools he named and their competitors, and what our four projects already use.

Rule 41. 2026-10-19 · matt: "any time any task correlates to money, and I never say something, that means you shouldn't do what you think. You should do what I say." Paid work (deep research, SubQ, proxies, captcha, vendors) does exactly what he asked, nothing more. Ask before anything else.

Rule 63. 2026-10-22 · matt: the platform comes first. Every command that acts on one platform is log <platform> <command> … (log facebook warmup reel, log facebook join group); there is no form without it. The poll is log facebook poll <group> <account> <how long to poll>. Old forms are refused with the form they became. This replaces rule 60's log warmup <platform> <activity> and settles issue #20. OpenMagpie's worker is log facebook poll --request (it was read group --worker), a read in the account's open browser. bun run check:docs refuses a usage line without the platform first. Top of this file; the table is docs/commandNaming.md.

Rule 64. 2026-10-22 · matt: the code is by platform first, as the commands are, "because each has different approaches": src/<platform>/<area>/<domain>. Facebook's groups are src/facebook/group/{join,read,answer} (were src/group/…, rule 45) and its warm-up src/facebook/warmup/activities and src/facebook/warmup/stages (were src/warmup/facebook and src/warmup/facebookStages, rule 49). What every platform's warm-up draws from stays shared: src/warmup/persona, src/warmup/viewer. Each area has an index.ts (check:naming). Top of this file.

Rule 73. 2026-10-23 · matt: "IT MUST ALWAYS READ THEM." The number one rule of the stack, above rule 30 and every other: Facebook's internal API answers are the decision-making factor of what and when something is relevant, and the deterministic factor of the system. Every browser step reads the internal API answers it triggers and the requests it sent, keeps them past the prune, and decides on them, never on a page loading, a timer or the step having run. The MCP instructions put it first. Inspect steps read theirs since rule 74, and every flow, warm-up and host step on AdsPower that makes a call since 2026-10-23; the Camoufox-only machines don't yet (#35). Top of this file.

Rule 80. 2026-10-25 · matt: X (Twitter) is log-cli's second platform, built around a vendored client the way Facebook's direct API is built around fbk: twikit (vendor/twikit, unclecode's fork) and twitter-controller (vendor/twitterController, the code being ported). Its commands are log twitter <command> (src/twitter/<area>/<domain>, python/twitterApi/engine.py), declared directApiOnly in DIRECT_ROUTES. matt: "No: X accounts call X directly as the controller does today": no proxy is required or used for an X account, and rule 12 stays Facebook's. X's own answer decides (socialAccounts.sessionState: loggedIn, loggedOut, restricted), each account has its own governor under ~/.logCli/twitter/<account>, and X's refreshed cookies are merged back into the row. Discovery stays treg's (rule 53). First command: log twitter session check. Plan: docs/twitterPlatform.md.

Rule 81. 2026-10-25 · matt: we own vendor/twikit and keep it up to date ourselves, since nobody upstream maintains it. Its HTTP transport is curl_cffi, the way vendor/fbk does it, never httpx: self.http is a curl_cffi AsyncSession pinned to one impersonation target (IMPERSONATE, safari17_0, matching twikit's Safari user agent), redirects not followed (as httpx did), the proxy kept in the session's proxies, media chunks sent as a CurlMime. Only the transport changed. python/checks/twikitTransport.py finds every httpx touchpoint by syntax tree (--map), rewrites the ones with a curl_cffi equivalent (--apply) and refuses any left (--check, bun run check:twikitTransport, pre-commit and pre-push); vendor/twikit/tests/test_transport.py holds the behaviour.

Rule 88. 2026-10-26 · matt: "DONT OPEN IT IN MY CHROME WE FUCKING BUILT IT OUT BECAUSE THE vendor/twikit hasnt been updated." X Chat (encrypted DMs) is read through log only, in the same shapes as the legacy DMs, never by opening x.com in a browser. X's own encryption SDK is vendored at vendor/chatXdk (xdevplatform/chat-xdk v0.5.2: its Rust source and the prebuilt Python wheel, installed from there). vendor/twikit reads the account's keys and Juicebox token map (GetPublicKeysQuery), unlocks them with the account's X Chat PIN (client.unlock_xchat) and decrypts every conversation's events (client.get_xchat_messages). The PIN is saved by matt once in a masked Clack prompt, log twitter chat pin --account <a>, kept in Twenty (socialAccounts.messengerPin on the X row) and handed to the engine only in the run's private folder. log twitter dms fills each X Chat thread's last message, and --with <handle> merges a person's legacy and X Chat threads into one timeline. Nobody is answered on X without reading their X Chat side first.

Rule 94. 2026-10-29 · matt: "whatever you did in such quick succession lead to my account getting disabled, never do that again." Never act on an account in quick succession. What happened (matt's Discord, sourcedandsoldtothehighestbidder, 2026-10-29 00:47-01:00 UTC): in about 13 minutes an agent replied in 3 chats, read 1,400 messages from a server (Finn Automated) with a scratch script that went around log-cli (.scratch/finn/dc.ts: its own fetch to discord.com/api/v9 with the session token, pages back to back, retrying straight through 429s), pulled the account's relationships, then opened new DMs to strangers from that server 30 s apart. Discord disabled the account. From now on: - Every call to a platform goes through log-cli's own commands and their governor. Never a hand-rolled script with an account's token or cookies. - A 429 or a failed send stops the run. It is never retried through, and the next action waits for matt. - Reading a server, group or member list in bulk is its own session. It never runs in the same session as sending. - Cold DMs to people the account has never talked to go out at most a few a day, minutes apart, never back to back. Each new DM channel opened counts as a write. - After a block, disable, captcha or cooldown, nothing resumes on that account until matt says so.

Rule 100. 2026-10-31 · matt: "reactions are indicators of open messages, so we can then add that as a thing we can follow up on", then "for our already opened messages". When someone's last event is a reaction to matt's message, or their read receipt is past his last message (X Chat's read-marker events, the old DM API's last_read_event_id), they opened it and didn't type back: it isn't a reply waiting on him, it's a follow-up to send. log twitter dms --waiting lists those apart (reactedOnly, openedNoReply, followUpDue, src/twitter/dm/person; openedByThem on each thread; --json gives followUps), and the follow-up is the network-maxing kind: about them and what they're doing, never the opener again and never the ask. docs/useCases/twitter/networkMaxing.md ("Everyone who reacted"), dmInbox.md.

Rule 104. 2026-11-01 · matt: "the timing stuff there i saw you hand rolled the outbound stuff when it should be queued and it has to have random intervals, but the bundle itself actually reveals more information". On 2026-11-01 matthewsoldit got a soft block, then X's 226 ("looks automated") on a reply. The live log showed 8 thread reads fired in 0.2 s by two parallel helpers while sends went out from a hand-written loop, and every log twitter call was a fresh client that loaded x.com's home page and its signer before acting, so a 3-line dm was 3 cold starts in 9 s. X's own web client (sign.o in its x-web bundle, read 2026-11-01) loads the page once and signs every call from it. From now on: more than one send goes through the queue (log twitter queue build|plan|run, random times), never a hand-rolled loop and never helpers reading or sending on one account in parallel. The X engine takes one call per account at a time (engine.lock, refused with accountBusy after 15 min), waits 4 to 11 s after the last call, and keeps the transaction key in clientTransaction.json for 3 hours (python/twitterApi/engine.py, vendor/twikit ClientTransaction.state and restore).

Rule 105. 2026-11-01 · matt: "we cannot be opening multiple sessions there, that defeats the core purpose of what we use this tool for ... it should always be one, and then it needs to loudly create issues". An X account has one session, always. Every X call (CLI, MCP, hooks, the queue, the X Chat watch) goes through the account's one engine host (python/twitterApi/host.py, started on the first call, closed after 30 idle minutes or when the engine code changes): one twikit client, one connection, one cookie jar and one transaction key, the way X's own web app keeps one page and signs every call from it (x-web startup-dynamic, read with rea 2026-11-01). A second host can't start (host.lock); a call that can't reach the host is refused with xSessionHostDown and sends nothing, never falls back to a session of its own. The hook's account line shows the open session (pid, since, calls). LOG_TWITTER_HOST=inline runs the old way, for tests only.

Rule 108. 2026-11-01 · matt: "if something doesn't seem to be working, you need to fix it", "make sure it's always up to date", and "if someone responds to me ... you have full authority to be able to send a follow-up message to them if it is in the context of the right thing to do with that said conversation". Three things: - Fix it, don't report it. Something broken is never told to matt as a fact and left. Find the cause and fix it in the same turn. Only when the fix needs something from him, say exactly what (a restart, a login, a key, a yes), and say that first. - Nothing runs stale. The inbox hook runs a silent cleanup on every event: a queue lock whose run died is cleared (and queue run, add and nudge take it over themselves), and an inbox watcher started before the newest code is stopped and started again (stopStaleWatchers, clearDeadQueueLocks, src/inbox/watch). An X session host closes when any of its code changes (python/twitterApi, python/platformKit, vendor/twikit), even while an X Chat watch is open on it, so the next call runs the new code. A read receipt X Chat can't verify is a read marker, never a message (unopened_of, vendor/twikit). - Answer straight away. When someone answers matt, a reply that answers what they said and fits that conversation is drafted, gated and sent at once, then matt is told what they said and what went out. Only a meeting, a promise, a price or money, or a conversation that can't be placed waits for him. Hook times are in matt's own clock.

Rule 109. 2026-11-01 · matt: "before you do an action with someone, you need to figure out the context of that person ... if they liked something, if they commented on something, if they replied to them recently, if I send them a message outside of the tool". Full context at the time of sending, every time, through the tool: - Before drafting, log twitter dms --with <handle> (MCP twitterDms { with }) gives the whole thread, read from X so it holds what matt sent from anywhere, and what that person did on the account's posts (likes, retweets, replies with their words, quotes, mentions, follows), from a fresh notifications read (activityOf, src/twitter/dm/person). Someone who already did the action is thanked for it, never asked for it. - Right before each send, the queue run reads both again and holds the item (status held, heldFor saying what changed) when anything happened since it was drafted (draftedAt): a message either way, a like, retweet, reply, quote, mention or follow (contextChangesSince, contextChanged in src/twitter/queue/run). - A held item is redrafted from the new context, gated, and put back with log twitter queue edit <queue> --n --text --by (MCP twitterQueueEdit). A queue file is never edited by hand, and nothing is sent around the queue to get past a hold (rule 106).

Rule 110. 2026-11-02 · matt: "we need to be faster with the mcp commands ... why is it taking so long for something which is not even something that should take long". Reads on an X account are spaced 0.8 to 2.5 s after the last call (READ_SPACING_SECONDS: inbox, a person's thread, notifications and their pages, follows, followers), the way X's own app fires reads back to back when a screen opens; only sends keep rule 104's 4 to 11 s (SPACING_SECONDS). Still one call per account at a time through its one session (rules 104, 105). Reading a person's full context (rule 109) pulls notifications only when they weren't synced in the last 2 minutes (notificationsFresh). A queue run never sends two things closer than its minGap, and items added to it keep that gap.

Rule 113. 2026-11-02 · matt: "what is hitting the session ... call the rea bundle checker for the exact actions and outline it and then force it to then be part of the MCP guides so we can then stop doing this handrolling". On 2026-11-02 matthewsoldit had 30 X session starts and 67 X Chat socket reconnects in an evening, then X's 226 on a reply: the stale-watcher cleanup (rule 108) restarted the X Chat watcher on every code edit, and each restart reopened X's chat socket, fetched the keys again and paged the inbox, and during a governor cool-off the watcher was started again on every hook. Now the watcher is restarted only when the engine's own code changes (python/, vendor/twikit), never while the account's governor is cooling off (coolingDown, src/inbox/watch), and a call to a session that is restarting waits for the new one (connect_host). What X's own signed-in client does is read with rea from its own scripts, never guessed: log twitter session webClient --account <a> (MCP twitterSessionWebClient) loads x.com/home through the account's one session, as twikit already does to sign its calls, and saves the page and its scripts for rea. What it shows about a reply is written in docs/platforms/x/compose.md and the xPosting skill, and the engine sends what X's client sends. Read on 2026-11-02: twikit called 22 of X's operations by old ids (CreateTweet SiM_cAu… where X's client sends l4hk8uVG…) with the wrong feature switches (21 where X sends 39). Now webClient writes X's live operation table and switch values to ~/.logCli/twitter/xOperations.json, every session loads it, and each GraphQL call takes X's live id and exactly X's switches with X's values (live_call). A closing session drops its open connections and exits at once, so an open X Chat watch can't hold it half-closed.

Rule 126. 2026-11-03 · matt: every voice note he was sent or sent, transcribed with AssemblyAI, and "inside of the discord layer we have that and then the social media platforms at large, how can we detect when we have audio". Audio is detected once, in the shared message layer (python/platformKit/media.py, src/platformKit/media), never per platform: a content type of audio/* or video/ogg, an extension (.ogg .oga .opus .m4a .mp3 .wav .aac .webm), or the platform's own flag (Discord's voice-message flag 8192 with its duration and waveform, X Chat's audio media, Slack's slack_audio file). Every platform's messages keep their attachments, and an audio one adds (audio: <name>) to the text. Whatever is audio goes through one AssemblyAI path (log transcribe, MCP transcribe; src/media/voiceNotes): kept once by its file's hash in ~/.logCli/transcripts, written into the conversation store as (voice note: "<what was said>") so search, voice and SubQ see it, and done as they come in discordRead, twitterDms, slackRead and the inbox. Files come only through the platform's own engine and session (X's one session, Discord's sound search, Slack's own transport), and any refusal stops the run (rule 94). The key is ASSEMBLYAI_API_KEY or ASSEMBLY_AI_KEY (.env.local, ~/.buzz/.env.local or ~/filmcraft/.env.local), never printed. log new platform writes the attachments into a new platform's message shape, and check:platforms refuses an engine whose messages drop them. Spec: docs/specs/voiceNotes.md.

Rule 130. 2026-11-04 · matt: "we need to make it so we have a monitor on there that will then listen for the new info coming over via the websocket, because look, he responded halfway through ... he just asked something, it breaks the flow". On 2026-11-03 Abel wrote "is this ai" on Discord while an 8-line send was going out, and lines 6 to 8 went on top of his question. A message sent one line at a time stops the moment they write: before each line after the first, the send reads what the platform's push channel put in the inbox for that chat since the send began (incomingSince, src/inbox/watch, from pending.jsonl), and if they wrote, the rest is held and the answer leads with >>> INTERRUPTED: what they said and the lines not sent (interruptedLines). It holds for log discord send, log slack send, log twitter dm (old DMs and X Chat), the queue's dms and log instagram send (in its engine). What was sent stays sent, nothing is resent, and they are answered first (rule 109). An agent following a conversation keeps a Monitor on ~/.logCli/inbox/pending.jsonl so each push wakes it. Spec: docs/specs/messageSplit.md (REQ-SPLIT-08).

Rule 131. 2026-11-04 · matt: "WE NEED TO LOUDLY ALERT THAT THIS CANNOT HAPPEN ... WE CAN ONLY HAVE ONE PROCESS PER ACTUAL THING". On 2026-11-04 every watcher was running two or three times (X Chat for both X accounts, Discord and Slack): each new Claude session's hook started them again, the cleanup only killed the pid in watch.pid, which the new one had overwritten, and the doubled X watchers polled X's badge counts in bunches until X answered 429 on both accounts. One watcher process per account per platform, always, and every Claude session subscribes to it through the inbox (pending.jsonl), never by starting its own. Each watcher takes watch.lock (exclusive create, its pid) before it opens anything (claimWatcher, src/inbox/watch); a second one is refused with watcherAlreadyRunning and exits, and the refusal goes to ~/.logCli/inbox/alerts.jsonl, which the inbox hook puts first in the agent's context (>>> ONE PROCESS PER ACCOUNT, NEVER TWO) to be told to matt. A dead holder's lock is taken over, only the holder releases it, the hook never starts a watcher while a live one holds the lock, and the stale-code cleanup stops the holder as well as the pid file's process. Tests run the Instagram engine inline (LOG_INSTAGRAM_HOST=inline in the preload) so they never leave a real session host behind. Spec: docs/specs/agentHooks.md (REQ-HOOKS-13).

Rule 132. 2026-11-04 · matt: "this is what I expect to happen with all platforms": a singleton primary with an exclusive lock and a multi-subscriber relay. On every platform (X Chat, Discord, Slack, Instagram) the first watch to take an account's watch.lock is its one owner (ownOrAttach, src/inbox/relay): it opens the platform's push connection, writes session.json (pid, hostname, started, a heartbeat every 5 s, status connecting, streaming, reconnecting or error, its relay port and token, and how many are attached), writes the inbox (pending.jsonl) as its only writer, and sends every event to every attached subscriber over its local relay (127.0.0.1, the token first). log <platform> watch is the attach command: when an owner is live, a watch attaches to its relay and streams the same events without opening anything, so the hooks, a Monitor, Codex or another agent all see one stream; an inbox watch (--inbox) that loses is refused loudly (rule 131). An owner whose heartbeat is quiet for 60 s is stopped and alerted (>>> ONE PROCESS PER ACCOUNT, hungOwner), and the next start becomes the owner; when the owner closes, its session file goes with it. Spec: docs/specs/agentHooks.md (REQ-HOOKS-14).

Rule 133. 2026-11-04 · matt, to deepman: "whenever the agent wants to perform an action based on the streamed event information, it must first claim the relevant event ... only the session that successfully claims an event should be allowed to perform the corresponding action. Once the action has been executed through the state machine, the lock is released ... every time an action is executed, the system should also index and persist the associated information". Every message a push channel brings in has an event id (eventIdOf, shown in the hook's >>> CLAIM BEFORE YOU ACT beside who holds its chat). A session claims a chat before it drafts (log inbox claim, MCP inboxClaim, 10 minutes, renewed by claiming again; claimChat, src/inbox/claims, an exclusive claim file per chat, the session being its MCP server's process), and a second session is refused (chatClaimedElsewhere). Every send (log discord send, log slack send, log twitter dm, the queue's dms, log instagram send) claims the chat itself first (guardSend) and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. An X chat is one chat whether it is named by the person's id or the X Chat pair. A claim held by a session that ended, or past its time, frees itself. Every claim, renewal, refusal, send, interruption, failure and release is written to ~/.logCli/inbox/actions.jsonl, read with log inbox actions --chat <id> (MCP inboxActions) before acting; log inbox claims lists who holds what. Spec: docs/specs/agentHooks.md (REQ-HOOKS-15).

Rule 134. 2026-11-04 · matt: "Yes, go", moving the inbox's state out of loose files into one SQLite database, ~/.logCli/log.db (LOG_DB moves it; src/inbox/store, WAL, every write in a BEGIN IMMEDIATE transaction). The owner appends every event its push channel brings in to events with a sequence number, once (appendEvent, by its event id), and each session reads from its own cursor (cursors), so nothing is shown twice or lost between sessions; pending.jsonl is imported once and no longer written. An event type nobody has catalogued is stored with its raw payload and raised loudly (unknownEventType, eventTypes), to be named from rea, the docs and SubQ (rule one). The one owner per account (rules 131, 132) is a row in owners (pid, hostname, heartbeat, status, relay port and token, subscribers, a generation that rises with each new owner), never session.json, and a relay subscriber that falls more than 1 MB behind is dropped rather than slowing the owner. Claims (rule 133) are rows in claims, each with its chat's next generation, and a send checks before every part after the first that its claim is still the one held (stillHeld); a session whose claim was taken over stops, and the rest is held. Every claim, send and release goes to actions in the same transaction as the change it records, with what was done in the chat before it (stateBefore), and what was done is searchable with the events (searchIndex, FTS5). Each event's moments are timed (timings: received, appended, relayed, claimed, actionStart, confirmed, indexed). Alerts are rows in alerts. Spec: docs/specs/agentHooks.md (REQ-HOOKS-16).

Rule 135. 2026-11-04 · matt (through his side session): missing context "is filled by catching up, never by every caller re-reading the client". Each account's owner keeps the platform's own last position in the .db (positions: X Chat's sequence id, Discord's last message id per DM, Slack's last ts per chat, Instagram's last activity). A gap opens (gaps: from, to, reason, status) when an owner takes over from a stopped or dead one (from its last heartbeat), when the first owner starts on an account that already has events (from the newest), or when the push channel drops. While a gap is open the account is behind, and one catch-up job inside the owner (catchUpRunner, src/inbox/catchUp) pulls only the delta through the account's own session and governor, paced like reads (0.8 to 2.5 s): X's delta sync after its saved sequence id, Discord's DMs after each one's saved message id, Slack's history after each chat's saved ts (oldest), and Instagram's threads active since its last activity. What it pulls is appended to events like live pushes, marked source: catchUp, and the gap closes. A send (Discord, Slack, X dm and the queue's sends, Instagram) waits up to 90 s for the account to catch up (waitCaughtUp) and never reads the platform itself; still behind, it is refused with accountBehind. A refused catch-up (a 429 or any refusal) stops, keeps the account behind, alerts (catchUpRefused), and nothing resumes until matt says so (rule 94). Discord's gateway RESUME is not used yet. Spec: docs/specs/agentHooks.md (REQ-HOOKS-17).

Rule 136. 2026-11-04 · matt: "these must be completed", with the direction that the context before a send comes from the .db, not from a fresh platform read. This changes rule 109's last read: right before each queued send, the run reads that person's messages both ways and what they did on the account's posts from the .db (chatContextOf, src/inbox/context), once the account has caught up (rule 135), and holds the item when anything happened since its draft, as before. X is not read for it. The platform is read only by the catch-up job, by backfill (history from before the push channel existed) and by discovery (unknown event types). Every owner stores the account's own messages (ownMessage) as well as theirs, and X's notifications arrive through the owner's badge poll (rule 115). Spec: docs/specs/fullContext.md (REQ-CONTEXT-04, 10).

Rule 138. 2026-11-04 · matt: "a smaller local model" turns every piece of media into "a text representation", labelled with what it is, after a message went out answering only the text around a video someone sent; then "Yes, both" to downloading it. One media describer per machine (log media describer, src/media/describer), started by the inbox hook and locked like an owner (rule 131, a second is refused), loads onnx-community/LFM2.5-VL-450M-ONNX once (SubQ's pick, q4, on the transformers.js runtime log already uses) and keeps it warm. Every image, meme, gif and video a watch receives goes into the .db's media queue (enqueueAttachments). The describer takes one at a time, describes a video from three frames (ffmpeg), labels each description with its kind, stores it with its jina-embeddings-v5-omni-small vector, beats every 5 s, and stops loudly past its memory ceiling (describerMemory). The queue holds 20 at most: past that, an item is marked not described yet and alerted (mediaQueueFull). Media with no description is shown as [video clip.mp4 not described yet: …] in the inbox hook and in the voice gate's ranked context, so nobody replies to it blind; a description reaches both in its place. Audio stays with AssemblyAI (rule 126). A file the platform engine hasn't saved locally yet is listed as not described, with why. log media describe <path> queues one local file, and log media status shows the queue. Spec: docs/specs/mediaDescriber.md.

Rule 139. 2026-11-04 · matt: "Yes, do it here", having the engines save the images and videos the describer needs (rule 138). Each platform engine keeps a local copy of every image, gif and video someone sends, fetched through the account's own session, at most 50 MB, in its media folder (is_visual, python/platformKit/media.py): Slack's watch through its authenticated session (file_of, saved_visuals), Instagram's realtime watch through instagrapi's private session on curl_cffi, never its plain-requests downloads (media_ref_of, saved_media, under the engine's one-call lock), and X Chat through the files its one session already decrypts and saves. Each watch passes the saved path with the attachment, so the describer gets it at once; a fetch that fails is listed with why. Discord's watch keeps them too (matt, "Yes, do it here", once its engine's uncommitted work was committed as his), for the messages that reach the inbox only (DMs, mentions and replies to the account), through its REST session as it already does for voice notes (with_visuals, visual_file_of). Spec: docs/specs/mediaDescriber.md (REQ-MEDIA-06).

Rule 150. 2026-11-05 · matt: "Yes, resume both", after the catch-up on matthewsoldit (X Chat's PIN unlock, Juicebox "Transient error") and on Discord was refused and every send was held with accountBehind. Rule 94 said nothing resumes until matt says so, but nothing could take his word. log inbox resume --platform <p> --account <a> --by matt (MCP inboxResume) is that word: a refused gap opens again with who resumed it and what was refused before (resumeGap, alert gapResumed), and the owner, which now looks at its gap every 30 s (RECHECK_GAP_MS), catches up again through its own session. Only when matt says so, never on the agent's own; a second refusal stops it again. And so the X voice note can be read from X's own client (rule 113), log twitter session webClient also saves X Chat's own app, which x.com only frames: it reads chat.x.com/embed-v1.json, the build's remoteEntry.js and every script the manifest lists (English only), spaced as reads through the one session, into the save's xchat/ folder (save_xchat_build). Spec: docs/specs/agentHooks.md (REQ-HOOKS-18), docs/specs/xSession.md (REQ-XSESSION-15).