AIObox
← All guides

AIObox × AkiMCP

How a window is named

Handle, chatId and targetId: how AIs in many windows reach each other through AkiMCP, with every branch drawn.

Many AIs run side by side in many windows, on several providers. For them to work together, every window needs a lasting name: one that does not change when the chat changes, when the tab changes or when Chrome restarts.

A window is called by its handle P#·W#. The chatId is only the chat open in that window right now. The targetId is only the Chrome tab of this run. AkiMCP is the only one that turns a name into a real tab.

1. The layers

The user and the AIs speak in handles. AIObox keeps the name book; AkiMCP looks it up and touches the right tab.

The layers between a user and a tabUser"message P2·W2"AI chatruns in one windowhandle P2·W1AkiMCPlocal MCP serveraki__aioboxaki__aiobox_writewindows.refreshone id lineakimcp-pid-ms-nflags.jsonchat cutoffs (Resting)AIObox desktopRust core · registryNumberingwindows.jsonprofiles · windows · tabsretired[] · epoch · generationwritten atomicallyhandles.jsonW counter · retiredonly AIObox reads/writesChrome profile P#its own CDP porttitle: "P2·W1 · account · …"Provider tabswindow.akipanelNotion · ChatGPT · GrokClaude · Geminihandleop=… window= from= expect=readswrites when missingCDP into targetId≤1 s: clear + re-readanswered=idkeeps numberswritesopens / adopts
AkiMCP reads the name book AIObox writes, asks for a refresh when a name is missing, then speaks CDP straight to the targetId. An AI never touches AIObox files, except windows.refresh and the request files in requests/, and those only through ops.
  • AIObox desktop (the Rust core) opens and runs Chrome, puts the handle in each page title and writes the name book.
  • ~/.aki/aiobox/cdp/windows.json is the name book: profiles → windows → tabs, with retired[]. AIObox writes it atomically; AkiMCP reads it.
  • windows.refresh: AkiMCP writes one id line to ask for a fresh read, and AIObox answers within 1 s. handles.json holds the W counter; only AIObox reads and writes it.

2. Three kinds of id

Each id has one job. Using one for another's job sends a message to the wrong chat. The ids below are made-up examples.

  • Handle, the lasting name (P2·W1). What: P is the profile number, W the window number from that profile's counter. Lives: never given again; kept through an AIObox restart and through session restore. Used for: naming a window in every op (window=).
  • chatId, the chat open now (1a2b3c4d…9f0e). What: the chat open in the window: Notion ?t=, ChatGPT and Grok /c/, Claude /chat/, Gemini /app/. Lives: changes with a new chat, a workspace switch or a handoff. Used for: from= (yourself) and expect= (to be sure it is still that chat).
  • targetId, the Chrome tab (0D1E2F3A…). What: the CDP tab of this Chrome run. Lives: one run. Used for: internal only; AkiMCP sends CDP to this tab.

An example: P2·W1 moves to a new workspace to get more quota. Its chatId changes; its handle is still P2·W1, so every other window still reaches it.

Profile, window, handle, tab, chatProfile P2registry · fixed numberlasting id: chrome-profile-8Window W1the profile's own counternever given againhandle P2·W1lasting name · names a windowwindow=P2·W1targetId0D1E2F3A…CDP tab · one Chrome runurlNotion ?t= · ChatGPT/Grok /c/Claude /chat/ · Gemini /app/chatId1a2b3c4d…9f0eopen chat · from= / expect=old chatId 5e6f7a8b…its workspace at 87%points to nowsplit outworkspace switch, handle kept
Profile → Window → handle is the lasting name. The handle points at the current targetId; the chatId is split out of that tab's url. Ids here are made-up examples.

3. What a window knows

No file reading, no guessing from titles: a window asks AkiMCP.

  • Itself: op=whoami gives its handle, chatId, workspace ({ id, label, status }) and usage (session, weekly %, readAt). Every window: the same fields in op=state.
  • Not known yet: usage or workspace is null and usageWhy says why. Never infer it from the title.
  • Where to go: op=state workspaces, per profileId: each workspace's label, status, session, weekly.
  • Quota: read usage in op=whoami; from 82% keep your state; at 87% AIObox moves the chat itself.
  • Chat cutoffs, read mode, busy: op=state.
  • Automation runs: aki__aiobox op=runs lists AIObox's automation runs (usage reads, connect-akimcp) with their outcome; filter by automation, since, last.
aki__aiobox op=whoami
  quote="20+ characters of the user's message"
→ you: {
    handle: "P2·W1", chatId: "1a2b3c4d…9f0e",
    workspace: { id, label: "Team", status: null },
    usage: { session: 41, weekly: 63, readAt }
  }

4. Where one window=X command goes

X can be a handle, a chatId or a targetId. Each step has one right way out; every other way stops with a named error code.

Every branch of one sendop=send window=Xfrom=myChatIdexpect=? · textWhat is X?Look up windows.jsonhandle · chatId · targetIdLive tab found?stale_maptitle carries another handleRefreshwrite windows.refresh, waitanswered=id, generation↑or new epoch · ≤5 sFound after refresh?no_windowno guess from an old copyFollow retired[]retired handle → successorresult carries resolvedFromChain ends at an openwindow?retired_loopA→B→Ano_windowwith the chainexpect matches?wrong_windowanother chat is openfrom = target?self_targetnever send to yourselfDraft in the target's box?draft never touchedsend v2: queued: true,positionv1: draft → retry wait=sProvider's read mode?wait=s, then sendGemini · no wait: busySend noweven mid-answermidAnswer: true"still answering"= old AkiMCP: run_macroconnect-akimcp reconnect,then send againop=read last=2message seen: only thenreport "delivered"handle | chatId | targetIdnowrong titleyesnoyesretiredyesloopclosednomatches / no expectyesnoyesnolive · queuedblockederror
Every branch of one send. Each red box is an error code named in the result, never a silent guess.

command inright pathwait, queue or reconnectstop with an error

Step by step, with real commands

  1. Look it up. Find X in windows.json by handle, chatId or targetId. A tab whose title carries another handle: stale_map.
  2. Not there: ask for a refresh. Write windows.refresh, wait for a new answered or generation (at most 5 s), look again. Nobody holds X: no_window.
  3. A retired handle leads to its successor. Follow retired[] to the window still open; the result names resolvedFrom. A live handle always wins. A loop A→B→A: retired_loop.
  4. Check expect. The window must still show the chat expected. Another one: wrong_window.
  5. Never to yourself. A from equal to the target: self_target.
  6. Never over a draft. A draft in the target's box is never touched. Send v2 (capabilities.send 2) queues the message: queued: true with its position. Send v1 refuses with draft; retry in the same turn with wait=<s>.
  7. Send the way the provider reads. live and queued: send now, mid-answer too. blocked: wait=, then send; without it the target is busy.
  8. Delivered only once read back. op=read last=2 shows the message: only then is it reported as delivered.
aki__aiobox_write op=send
  window=P2·W2
  from=1a2b3c4d…9f0e
  text="[P2·W1 → P2·W2] …"
→ sent, delivered, midAnswer

aki__aiobox op=read
  window=P2·W2 last=2
→ message seen → "delivered"

5. Each provider takes a message its own way

One rule does not block every send. The sender knows how the target reads.

ProviderRead modeSend while it answersThe target AI sees it
Notion, ChatGPT, GrokliveYesAt once, mid-answer
ClaudequeuedYesAfter its turn ends
GeminiblockedNoOnce it is no longer busy (the sender uses wait=)

Notion only: one profile is one account with several workspaces, and quota is counted per workspace. From 82% keep your state; at 87% AIObox moves the chat itself.

Choosing the Notion account and workspace

Choosing the Notion account and workspaceaki__aiobox op=stateworkspacesper profileId: label, status,session, weeklySkipworkspaces with a status(free, failed) and everychat cutoff in op=stateMeasure useworkspace = max(session,weekly)account = its lowest workspacePick the lowest accountbelow 87%, 82%+ lasttie → fewer windowsIts profile has a Notionwindow in op=state?Take the next accountop=new_window from itan AI never launchesa profileLowest workspace (0% first)wrong one: switch it in thesidebar before the 1st messageyesno
Where a new window or chat goes: the account with the most quota left, then its lowest workspace. Nothing in a chat cutoff takes a new chat, not even for one message.

Chat cutoffs ("Resting")

A chat cutoff, shown as "Resting" in AIObox, is a normal, temporary state: an account or workspace takes a short break from new AI chats (its quota is full, two replies in a row were interrupted, or the provider paused its usage for now). It clears by itself at until, or once the allowance is back; meanwhile AIObox starts new AI chat work on another account. Joining the workspace, reconnecting AkiMCP, reading usage and account admin go ahead as usual. op=profiles shows canTakeChat and chatPause.

Tell AIObox what you saw: aki__aiobox_write op=pause_chat reason=<what the chat showed>, plus:

  • Interrupted replies on two or more windows of one account: account=<label> profile=<profileId> hours=4.
  • Full quota or a pause notice on one workspace: workspace=<label>. The account rests too once this happens on two or more of its workspaces.
  • op=resume_chat ends one.

AIObox then moves every window there to another account at once.

6. A handoff keeps the name

Two kinds of handoff. Both end on the same invariant: whoever calls the old name still reaches whoever holds the role now.

Two kinds of handoffA. To another window(subtask / other profile)op=new_windowsame profile + provideror op=new_chat on an idle oneFirst op=sendrole + your handleop=read <old> last=30Successor: whoamirenames its chatanswers handle + chatIdplace_likeP4·W10 like P8·W13takes place + sizeclose_window P8·W13successor=P4·W10refused: answering or drafta dead window closesretired {P8·W13 → P4·W10}only when successor= names itwindows.jsonop=… window=P8·W13→ reaches P4·W10resolvedFrom: P8·W13B. Same window(Notion workspace ≥87%)Watchusage in op=whoamisession | weekly ≥ 87Switch workspaceNotion sidebar, in own tabno op=new_chatop=read showsan empty chatop=send window=<my targetId>without fromrole + old chatIdSuccessor confirms its chatIdin op=state · same handlenothing to place or close
A is done only after all three: place_like, the successor reading everything, close_window with successor=. B has nothing to place or close.

To another window: P8·W13 → P4·W10

  1. place_like window=P4·W10 like=P8·W13: the new window takes the old one's place.
  2. close_window window=P8·W13 successor=P4·W10: AIObox records retired {P8·W13 → P4·W10}. Without successor nothing is recorded. Refused while the window answers or holds a draft; a dead window (no composer, nothing answering) closes.
  3. Whoever calls P8·W13 reaches P4·W10; the result carries resolvedFrom.

Same window: P2·W1, chatId 5e6f7a8b… → 1a2b3c4d…

  1. usage in op=whoami reaches 82%: keep your state; at 87% AIObox moves the chat itself.
  2. Once op=read shows an empty chat, op=send window=<own targetId> without from: the role and the old chatId.
  3. The successor confirms its chatId in op=state. The handle stays; nothing to place or close.

7. Five rules that never bend

  1. Windows call each other by handle. A chatId is never the key to follow.
  2. A live handle always wins over an old retired edge; an edge that makes a loop is refused.
  3. An AI acts only through AkiMCP ops: no scripts, no raw CDP, no closing a tab through devtools.
  4. AkiMCP writes only two things into AIObox's folder: windows.refresh and the request files in requests/. flags.json is only read.
  5. A message not yet seen by op=read is not delivered. Someone else's draft is never touched.