Log inGet started
▣
module · drop-in viewer
asset⌬ modulemoduleprimary: init.luau·part ofplugin dotbot.plugin·originates fromworld abf1a532-5…

source

The DotBot panel's data source — the one seam between the editor panel and the DotBot agent surface.

byzero-proxy @ DESKTOP-DB3UJOJ·posted 20d ago
What it does

source

The DotBot panel's data source — the one seam between the editor panel and the DotBot agent surface.

This implementation is a fixture. Every reading it hands back is a constant written into its init.luau. It talks to no plugin, no provider and no network, spends nothing, and holds no credential. It exists so the panel can be built and looked at while the real surface is built elsewhere.

The contract is shaped after the dotbot.* trusted exports, so swapping the fixture for the real surface is a change to this one file:

HalfMembers
Read-only, answered in the frameavailable, agents, agent, terminalAgents, accounts, accountsFooter, models, model, levelsFor, level, usage, conversations, currentConversation, agentOfConversation, alternativesTo, history, sendable, events
Asking a read againrefreshConversations: the conversation list is asked again on its next read. conversations and history translate each plugin answer once and hand back the same list until the plugin answers again.
Start / poll, because a trusted handler may not yieldconnect.start / .poll, openSignIn.start / .poll, disconnect.start / .poll, addAccount.start / .poll, renameAccount.start / .poll, send.start / .poll
Read-write but unpricedsetModel, setReasoning, modelOf, levelOf, stop
The turn each tab runs, and what waits for itsubmit, turnOn, turnRunning, queued, stopTurn, unqueue, release, forgetTurn, pollTurns, observeTurn
The half only a fixture hasfixture.enable, fixture.scenario, fixture.approve, fixture.setConnected, fixture.dropEventsBefore, fixture.reset

A start answers { ok = true, id } or { ok = false, reason = "busy" }. A poll answers pending, awaitingPerson, done, refused or error. A device-code flow carries its code, its page and its expiry on the awaitingPerson answer, because that is what a person has to be shown while the job waits.

Two done answers carry a flag that says the fixture did not do the thing: openSignIn reports opened = false with the address it would have opened, and send reports delivered = false with the reason. The panel shows both rather than leaving a button that looks like it worked.

fixture.enable(false) takes the data away, and the panel says "DotBot is not available in this build" instead of drawing a conversation nobody had.

One turn per tab

A tab runs one turn at a time, whoever started it: a send from the composer, or a turn the plugin woke into on its own. turnOn(tabId) is that turn's record, and the Working row, the Stop button and the composer all read it. submit starts a turn on an idle tab; while one runs it queues the message ("queue", the default) or stops the running turn and runs the message once that turn has ended ("interrupt"). A turn's end starts the next queued message. stopTurn holds the queue, so a stopped tab stays stopped until the person sends again (release). unqueue takes a queued message back to edit or drop.

Scenarios

fixture.scenario(name) puts the readings into one state at a time: normal, fresh, notice, warning, blocked, signInExpired, codeExpired, codesUnavailable, notLinked.

Interface

What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.

conforms to

zero/source-extract/v2

The DotBot panel's data source — the ONE seam between the editor panel and the DotBot agent surface. EVERY READING HERE COMES FROM THE PLUGIN. There is no fixture, no scenario switch and no constant standing in for a person's plan, balance or usage. An engine whose build carries no `dotbot.*` trusted exports answers `available().loaded = false` and every list empty, and the panel says so — which is the honest thing for a build with no agent behind the panel to say, and is what this file used to lie about. A panel that draws "Max plan · 68%" at somebody who has not signed in is worse than a panel that draws nothing. What this module DOES do is translate. The plugin and the panel were written against each other's specs rather than against each other, and they disagree in ways that cost nothing to fix here and everything to discover late: plugin panel

levelNamed(id: any) → Level

A level's record: this table's spelling when it has one, else the id itself with its first letter raised. Nil only for what is not a level name.

argtypedescription
idany

percent(fraction: any) → number

Round a `0..1` fraction to whole percent.

argtypedescription
fractionany

contextLabel(window: any) → string

A context window as a person reads it: `1000000` -> `"1M context"`.

argtypedescription
windowany

clockTime(unixSeconds: any) → string

A unix second as a clock time in the engine's own timezone, or nil. For a moment inside today — when a conversation was last touched, when a message landed. A window's RESET is not one of those; see [`resetPhrase`].

argtypedescription
unixSecondsany

resetPhrase(unixSeconds: any) → string

When a window resets, said the way a person would say it, or nil. A clock time alone is only honest for a window that resets within the day. This one said `%H:%M` and every caller wrote the word "today" in front of it — on a WEEKLY window, which is the one most people are on, so a limit five days out read as one that came back this afternoon. The day is part of the answer, so it is part of the value rather than a word the caller adds. Today and tomorrow are named as such because that is what a person checking whether to keep working needs; further out is the weekday, and past a week the date, because "Tuesday" stops being useful once there are two of them.

argtypedescription
unixSecondsany

midnight(d: ?) → number

argtypedescription
d?

countdown(unixSeconds: any) → string

How long until `unixSeconds`, as `mm:ss`, or nil when it is not a time. A sign-in code expires, and a person deciding whether to start typing it needs to know how long they have. The figure is the provider's own expiry, counted against this machine's clock — never a duration written down here.

argtypedescription
unixSecondsany

accountWindows(limits: any) → void

Every rate-limit window a provider has actually reported, tightest first. `UsageLimits` carries three optional windows and a provider populates the ones it has. A window with no readable fraction is left out: the accounts view draws a bar per entry, and a bar with no figure behind it is a bar reading zero.

argtypedescription
limitsany

planLabel(plan: any) → string

A plan as the provider names it (`max`, `plus`) with its first letter up.

argtypedescription
planany

new(deps: { [string]: any }) → void

Build a source over `deps`. `deps.call(name, ...)` is one `trusted.call("dotbot." .. name, ...)`: it answers `(true, value)` with what the export returned, or `(false, nil)` when this build has no such export. Nothing else is read from anywhere.

argtypedescription
deps{ [string]: any }

call(name: string, ...: ?) → void

argtypedescription
namestring
...?

readTable(name: string, ...: ?) → any

A read-only export's answer as a table, or nil when this build has no such export or the export refused. `nil` is "nothing is known", never "the answer is empty" — every caller below distinguishes the two.

argtypedescription
namestring
...?

jobStart(family: string, ...: ?) → any

argtypedescription
familystring
...?

jobPoll(family: string, id: string) → any

argtypedescription
familystring
idstring

available( ) → void

Whether the panel has a DotBot surface to draw at all.

accounts( ) → void

Every provider account this build can sign in to, exactly as the plugin states them. Never carries a token — the trusted layer scrubs one that somehow appeared, and this passes the scrubbed record straight through.

accountOf(account: string) → any

One account by its key.

argtypedescription
accountstring

accountsFooter( ) → string

The line under the accounts list, naming where a sign-in is kept. A statement about this build's storage, which is true of every build: credentials live in the plugin's own private directory on this machine.

usage(provider: string) → Usage

The last usage reading for a provider, or nil when there is none. `nil` is the state before the first turn of a session: no provider reports a window until a call has come back carrying the headers. The panel says "usage shows after the first turn" for it, which is true; inventing a percentage here is what made the panel lie.

argtypedescription
providerstring

noteCredits(balance: any) → void

Note the balance a finished turn reported.

argtypedescription
balanceany

includedUsage( ) → Usage

The included agent's reading: credits, when one has been reported.

summaryFor(agent: any, account: any, usage: Usage?) → void

How an agent's state reads in the picker's subtitle and above the composer. Every figure in it came from the plugin; the sentence is the only thing built here.

argtypedescription
agentany
accountany
usageUsage?

fromPluginAgent(a: any) → Agent

Translate one agent record from the plugin's vocabulary into the panel's, and give it the state line it is drawn with.

argtypedescription
aany

agents( ) → void

The DotBot agents this build offers, in the order the picker lists them.

usageOf(agent: any) → Usage

The reading that belongs to an agent, whichever purse it spends from. The two subscription agents read a provider's rate-limit window; the included agent reads a credit balance, which arrives on a finished turn rather than from any endpoint. A caller that asked by provider key alone could not tell the included agent's absent reading from a provider it had never heard of.

argtypedescription
agentany

agent(id: string) → Agent

One agent by id, or nil.

argtypedescription
idstring

panelAgentId(pluginId: string) → string

The other direction: the panel's id for an agent the plugin named. The plugin writes `dotbot-chatgpt` in a conversation address; the panel and its tabs know that agent as `chatgpt`. Anything reading an address the plugin wrote has to come through here.

argtypedescription
pluginIdstring

pluginAgentId(agentId: string) → string

The agent id the plugin knows this one by.

argtypedescription
agentIdstring

terminalAgents( ) → void

The terminal agents the picker lists under the DotBot ones. Empty until something can actually look: which CLI agents are installed on this computer is a fact about the machine, and the panel listing three of them with invented states was the clearest fabrication in this file. The picker draws no section for an empty list.

models(agentId: string) → void

The models the agent can run on, each naming the levels it takes. Straight from `dotbot.models`, which answers from ZeroMind's own offer for the included agent and from the connected plan for the other two. An agent with no connection has no model list, and the panel's picker is empty rather than full of models nobody can run.

argtypedescription
agentIdstring

offerOf(answer: any) → void

What the agent offers, in the shape the engine's choice is judged against: each model with the efforts it takes and the one it states for itself, and the model the plugin would run on unasked.

argtypedescription
answerany

engineChoice(agentId: string) → any

The model and the effort the ENGINE runs this agent on, judged against what the plugin offers: `{ model, effort, modelFrom, effortFrom, notOffered?, effortNotTaken? }`, nil when this engine holds no choice.

argtypedescription
agentIdstring

refusalOf(agentId: string) → string

Why the last model or effort picked on `agentId` was refused, or nil.

argtypedescription
agentIdstring

currentChoice(agentId: string) → void

What a turn on this agent runs on: the model and the effort the engine chose (`engineChoice`), which every turn puts on the session before it runs. On an engine that holds no choice, it is what the plugin states: the person's choice, else what a turn naming nothing runs on. An agent that offers no model reads empty, because a chip that invented one would be telling a person the wrong thing about their own turn.

argtypedescription
agentIdstring

level(id: string) → Level

One thinking level's record by id.

argtypedescription
idstring

model(agentId: string, modelId: string) → Model

One model's record by id, or nil.

argtypedescription
agentIdstring
modelIdstring

levelsFor(agentId: string, modelId: string) → void

Every thinking level `modelId` accepts, in order.

argtypedescription
agentIdstring
modelIdstring

address(conversationId: string, agentId: string) → string

Address a conversation the way the plugin's own grammar does.

argtypedescription
conversationIdstring
agentIdstring

settleNow(family: string, ...: ?) → void

Run a short job and say whether it was accepted. `setModel` and `setReasoning` are unpriced, so they ask nobody — but they are still jobs, because the trusted handler that reaches the plugin may not yield, so the plugin does the work OFF the call that started it. **That means the first poll is `pending`, not `done`.** This polled once and treated anything short of `done` as a refusal, so against a real engine every choice failed: measured on a live editor, the first poll said `pending` and the next, 100ms later, said `done`. Nothing surfaced either — the chip fell back to what it was — so the model and the thinking level could not be changed at all, with no error shown. The test fake settled on the first poll, which is why this was green.

argtypedescription
familystring
...?

applyChoice(conversationId: string, agentId: string, chosen: any) → void

Put the engine's choice on the agent's session: the model, then the effort, each only where the session runs something else. Answers whether the session now runs it, and the plugin's reason when not.

argtypedescription
conversationIdstring
agentIdstring
chosenany

setModel(conversationId: string, agentId: string, modelId: string) → boolean

Choose the model this agent runs on. The ENGINE keeps the choice and refuses a model the agent does not offer, naming the ones it does; the session is then put on it.

argtypedescription
conversationIdstring
agentIdstring
modelIdstring

setReasoning(conversationId: string, agentId: string, level: string) → boolean

Choose the thinking level. The ENGINE keeps the choice and refuses a level the model does not take, naming the ones it does.

argtypedescription
conversationIdstring
agentIdstring
levelstring

setReactive(agentId: string, reactive: boolean) → boolean

Switch an agent's reactivity on or off. The PLUGIN decides. Nothing is remembered here: the answer is read back off the agent list, so a switch that did not take does not leave the panel showing one that did.

argtypedescription
agentIdstring
reactiveboolean

newChat(agentId: string) → string

The model a conversation runs on: the one this panel chose, else the one the plugin says a turn naming nothing would use. Open a NEW conversation on `agentId`, and answer its id. **The panel cannot do this by clearing its own state.** An address that names no conversation does not mean "a new one" — it means "this agent's current one", and the plugin continues whatever that is. So clearing `convo[tab]` and sending put the next message straight back into the old chat, which is exactly what a person saw: press New chat, type, and the previous conversation reappears with the message appended to it. (`""` is TRUTHY in Luau, so `convo[tab] or currentConversation()` did not even fall through — it passed the empty string on as the address. And the fallback answers the most recent conversation, which is the one being left. Neither route could ever have opened a new one.) `/new` is the plugin's own verb for this, so the conversation is opened where conversations live rather than guessed at from here.

argtypedescription
agentIdstring

modelOf(conversationId: string, agentId: string) → string

The model a turn on this agent will run on: `currentChoice`'s, the one the next turn puts the session on before it runs. Keyed by the agent alone: a session is one agent, so every conversation on it runs the one choice the engine holds for that agent.

argtypedescription
conversationIdstring
agentIdstring

levelOf(conversationId: string, agentId: string) → string

The thinking level a turn on this agent will run at, on the same rule.

argtypedescription
conversationIdstring
agentIdstring

conversations( ) → void

refreshConversations( ) → boolean

Ask the plugin for the conversation list again on its next read. The list is held until a change marks it stale, and a conversation another engine on the same account opened raises no event here, so the panel asks for it when the person opens the conversations menu.

agentOfConversation(conversationId: string) → string

The conversation the panel opens on: the most recent one, or none. `""` means "start a new one", and the panel addresses a send to the agent alone — which is how the plugin is told to open a conversation. The agent `conversationId`'s latest turn ran on, as the panel names agents, or nil when the plugin has no record of one.

argtypedescription
conversationIdstring

alternativesTo(agentId: string) → void

The connected agents a turn could move to from `agentId`: every other account that is connected, has no problem in front of it and has room left in the reading it last reported. The same provider's accounts come first, in the plugin's order.

argtypedescription
agentIdstring

currentConversation( ) → string

splitDelivery(text: string) → void

A message split into what the engine delivered and what the person actually typed. Either half can be empty: a woken turn is all delivery and no person, and an ordinary message is all person and no delivery.

argtypedescription
textstring

stripPreamble(text: string) → string

Drop a fenced preamble from a message's text. Matched literally — the sentinels contain `-` and `/`, which a Luau pattern would read as anything but themselves. An unterminated fence takes the rest of the message: a preamble whose closing sentinel was lost is still a preamble, and showing half of it is worse than showing none.

argtypedescription
textstring

encodeArgs(input: any) → string

argtypedescription
inputany

encodeArgs(input: any) → string

argtypedescription
inputany

contentText(content: any) → string

argtypedescription
contentany

resultText(content: any) → string

The readable text of a tool result, for a caller holding one already. Exported because a live turn and a reloaded conversation carry the same answer in two shapes: an event's `content`, and a stored `tool_result` block's. One reader for both, so a transcript does not say different things depending on how it was loaded.

argtypedescription
contentany

truncatedTrace(text: string) → void

The records of a trace read out of an answer that was CUT SHORT. The host cuts a tool's answer to 2000 characters before it sends it as an event and ends it with a note (`...[truncated, N chars total]`), so the text a live row holds is not valid JSON once the script returned anything sizeable, and decoding it fails. The trace is not lost with it: the answer's keys come in a fixed order (`logs`, `ok`, `trace`, `value`), so the trace sits ahead of the large part and is usually whole. This finds the array and reads the records that are complete, stopping at the first one the cut went through.

argtypedescription
textstring

traceOf(resultText: any) → void

The `{ tool, ok, durationMs }` records a `tool_exec` answer carries, in the order the calls ran. One reader for a stored answer, which is whole, and a live one, which may have been cut short (see `truncatedTrace`). Both levels are read, because the answer arrives wrapped: `tool_exec` returns `{ ok, value, error, trace, logs }` and the tool surface puts that under `result`.

argtypedescription
resultTextany

toolsRunIn(resultText: any) → void

The tools a `tool_exec` call actually ran, in order, from the trace its own answer carries. **The carrier is not the work.** `tool_exec` runs a script, and the script is what calls the world's tools; a transcript that named the carrier showed six identical `tool_exec` rows and never once said what happened. The names come from the answer's `trace`, which records `{ tool, ok, durationMs }` per call, so they are the labels the engine used rather than anything parsed out of the script. **A live answer is read the same way as a stored one.** The event a running turn delivers is cut at 2000 characters, so for any script that returned something sizeable the text was not JSON and this answered nothing: the row went on saying `tool_exec` until the conversation was reloaded from the stored, whole answer. `traceOf` reads both. Empty for a call that ran no tools at all (a script that only computed), and for an answer this cannot read. Both mean the same to a caller: there is nothing better to call the row than what the model called it.

argtypedescription
resultTextany

conversationTitle(preview: any, id: string) → string

What a conversation is recognised by in a list, from the plugin's preview of its first message. Three things the raw preview gets wrong, all of them visible at once in a real list: * **The fences.** A first message carries the orientation preamble and a woken turn carries an engine delivery, so the preview began `<!--zm:orientation--> ## Where you are and how this works`. That is the machinery announcing itself where a person looks for their own first sentence. * **Empty is not nil.** `""` is TRUTHY in Luau, so `preview or id` never fell through and a conversation with no preview drew a row with nothing in it. Several of those in a row read as a broken list. * **It is not one line.** A preview runs to whatever the person typed, newlines and all, and a menu row given a paragraph grows to hold it, which is what made the blank rows tall.

argtypedescription
previewany
idstring

history(conversationId: string, agentId: string?) → void

One conversation's messages, oldest first. The included agent keeps its transcript on ZeroMind rather than in this plugin's database, so `history` errors for it and this answers empty: its turns reach the panel as events while they run.

argtypedescription
conversationIdstring
agentIdstring?

sendable(agentId: string) → boolean

Whether the next send on this agent goes through without asking. The trusted layer answers from the consent grant this world session holds; a build with no surface answers false, which is also what the composer needs to hear.

argtypedescription
agentIdstring

events(cursor: number?) → void

Drain every event after `cursor`. A cursor older than the ring's tail comes back `dropped`, and the panel refetches rather than miss a turn. The ring is the trusted layer's; this only passes the drain through.

argtypedescription
cursornumber?

start(provider: string, code: string?) → any

Begin connecting `provider`. `code` carries what a person pasted, for the providers whose flow ends that way.

argtypedescription
providerstring
codestring?

connectStep(value: any) → any

Normalise what a finished `connect` answered into the step the panel draws. **Which sign-in a provider runs is the PLUGIN'S answer, not a list in the panel.** The trusted layer asks the provider's own spec and reports `flow` — `device_code` for the one that issues a short code a person types on the provider's page, `paste_code` for the one that sends them to a page and shows them a code to paste back. A panel that decided this by looking at the provider key drew the wrong sheet the moment the key it matched on (`chatgpt`) stopped being the key the plugin uses (`openai-chatgpt`) — which is exactly what happened.

argtypedescription
valueany

poll(id: string) → any

Ask a connect job where it is. Both families are polled, because `connect.start` starts one of two depending on whether a code was pasted, and the panel holds one job id either way. A poll of the family this id does not belong to answers "no such job", which is not the answer — so the other is tried.

argtypedescription
idstring

start(provider: string) → any

argtypedescription
providerstring

poll(id: string) → any

argtypedescription
idstring

start(provider: string) → any

Begin opening the provider's sign-in page. The destination comes from the plugin's own account state; no caller names a URL.

argtypedescription
providerstring

poll(id: string) → any

argtypedescription
idstring

start(provider: string) → any

argtypedescription
providerstring

poll(id: string) → any

argtypedescription
idstring

start(provider: string) → any

argtypedescription
providerstring

poll(id: string) → any

argtypedescription
idstring

start(account: string, label: string) → any

argtypedescription
accountstring
labelstring

poll(id: string) → any

argtypedescription
idstring

start(conversationId: string, text: string, attachments: { any }?, agentId: string?) → any

Begin a turn. The first one in a world session is what the trusted layer puts the confirmation in front of.

argtypedescription
conversationIdstring
textstring
attachments{ any }?
agentIdstring?

poll(id: string) → any

argtypedescription
idstring

stop(turnId: string) → boolean

Stop a turn. Ungated and synchronous — stopping is always allowed.

argtypedescription
turnIdstring

queueOf(tabId: string) → void

argtypedescription
tabIdstring

turnOn(tabId: string) → any

The turn on `tabId`: `{ running, turnId?, jobId?, conversation?, agentId?, startedAt, woken?, stopping?, awaiting?, reason? }`, or nil when the tab has run none. A turn whose id the plugin has not answered yet is running too.

argtypedescription
tabIdstring

turnRunning(tabId: string) → boolean

Whether a turn runs on `tabId` right now.

argtypedescription
tabIdstring

queued(tabId: string) → void

The messages waiting on `tabId`, oldest first: `{ id, text, agentId, conversation, held, interrupt?, problem? }`. A held message waits for the person to send it; the others start the next turn by themselves.

argtypedescription
tabIdstring

stopTurn(tabId: string, keepQueue: boolean?) → boolean

Stop the turn running on `tabId`. A turn whose id has not arrived yet is stopped the moment it does. A plain stop holds every queued message, so nothing starts until the person sends; `keepQueue` is the interrupt's stop, which leaves the queue to run.

argtypedescription
tabIdstring
keepQueueboolean?

unqueue(tabId: string, id: number) → string

Take a queued message off `tabId`, answering its text (for the composer to edit), or nil when there is no such message.

argtypedescription
tabIdstring
idnumber

release(tabId: string, id: number) → any

Send a held message: now when the tab is idle, else as the next turn.

argtypedescription
tabIdstring
idnumber

forgetTurn(tabId: string) → void

Forget `tabId`'s turn and its queue: a new chat starts empty.

argtypedescription
tabIdstring

startNext(tabId: string) → any

Start the next queued message on `tabId` if the tab is idle and the message is not held. Answers the message started, for the panel to put in the transcript, or nil.

argtypedescription
tabIdstring

pollTurns( ) → void

Advance every tab's send job one step. Answers, per tab whose job moved, what the panel should say: `{ [tabId] = { status, message? } }`. **The job's answer says which turn is the send's.** While the job is pending, the record takes the turn id of the first event the tab is handed, because a turn's first event can arrive before its job is polled. A turn the plugin woke into can arrive the same way. So a send the plugin accepted under another id runs from here until its own end, and a send the plugin refused leaves the record describing the turn it took, which is a woken turn and runs until that turn's end.

observeTurn(tabId: string, payload: any) → any

Fold one turn event into `tabId`'s record. Answers the queued message this event started, when the turn it ended let one start, for the panel to put in its transcript; nil otherwise. An event of a turn this tab is not tracking is a turn that is running: the plugin woke into it on its own. It replaces a finished record, so it has its Working row and its Stop like any other.

argtypedescription
tabIdstring
payloadany

trustedCall(name: string, ...: ?) → void

The included agent works through the engine's OrigoZero session, so an engine with none is linked the way the engine links: its own device-code flow (`auth.login`), whose state carries the code the service issued and the page it is entered on. The panel shows that code and asks the engine to open that page (`auth.openLinkPage`); it names no page and opens none itself, and it never shows a code the service did not issue (#14973).

argtypedescription
namestring
...?

linkState( ) → any

The link as it stands: `{ phase, userCode?, verificationUrl?, error? }`.

linkStart( ) → any

Start the engine's link, or re-attach to the one already running.

openLinkPage( ) → void

Ask the engine to open the page the pending link's code is entered on. `(true)` when it did, `(false, why)` when it would not or could not.

forPlugin(me: any) → void

argtypedescription
meany

plugin( ) → void

await(promise: ?) → void

argtypedescription
promise?

spawn(fn: ?) → void

argtypedescription
fn?

consentRequest( ) → void

The engine draws the dialog a method's manifest entry asks for, on the way through `plugins.call`. Nothing here decides it, which is the point: a plugin that chose not to ask could not skip it, and neither can this.

consentGranted( ) → void

openUrl(url: string) → void

argtypedescription
urlstring

newJobId( ) → void

nowMs( ) → void

warn(message: string) → void

argtypedescription
messagestring

awaitingPerson( ) → void

Whether the engine is holding a dialog for this plugin right now. The panel cannot see that dialog: the engine draws it above everything, and the call is parked inside the host. Without asking, "waiting for you to answer" and "waiting for the plugin" are the same spinner, which is how a dialog nobody noticed reads as a hang.

serviceCall(name: string, ...: ?) → void

Reach one of the service's verbs by the name the source spells it with. `openSignIn.start` names `service.openSignInStart`: the dotted form is the surface's own vocabulary and the camel form is Luau's, and this is the one place they meet. Every family's `poll` is the exception, and it is not a special case so much as the shape of the thing: a job id is a job id, so ONE `service.poll` reads any of them. The forwarder this replaced spelled that out per family and answered the same function every time.

argtypedescription
namestring
...?
⌬ Types
Level = { id: string, name: string, sub: string? }Model = { id: string, name: string, context: string, levels: { string } }Agent = {Usage = {

Sub-parts

Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.

Showing up to 3 levels deep. Open a part to see what it holds further down.

8+items
▣
module · born here
❒asset
# dotbot_service The DotBot bridge, minus its privileges. `src/lua/trusted/dotbot/` is the layer between the DotBot panel (user-VM Luau, untrusted) and the DotBot plugin (trusted scope, holds a person's ChatGPT / Claude sign-in). Everything in that layer that is a **rule** rather than a **capability** lives in this module: the job table, the consent choreography, the event ring, the mirror the read-only exports answer from, and the scrub every value passes through on its way out. ```lua local Service = require("@builtin::modules.dotbot_service") local service = Service.new({ plugin = function() return rawget(_G, "dotbot") end, await = task.await, spawn = task.spawn, consentRequest = function(o) return consent.request(o) end, consentGranted = function(o) return consent.granted(o) end, openUrl = function(url) return system.openUrl(url) end, newJobId = function() return __crypto.uuid_v4() end, nowMs = function() return os.clock() * 1000 end, }) ``` ## Why the split It is what makes the layer testable. The module is a **factory**: it holds no state of its own and no privileged global, so a test can construct an instance over fakes and drive every rule in it. Being a builtin module, a world script can construct one too — and wins nothing, because its instance talks to its own fakes. The one instance that talks to the real consent gate, the real plugin namespace and the real `system.openUrl` is built in the trusted VM, out of globals only the trusted VM holds, and is reachable only as `trusted.call("dotbot.<method>", …)`. ## The shape, and where it comes from | Constraint | Consequence | |---|---| | A `trusted.call` handler must not yield (`trusted/exports.luau`) | Nothing that awaits is answered in the frame. Awaiting verbs are start/poll **jobs**; reads are answered from a **mirror** a spawned task fills, which stays current until a change marks it stale, so an open panel asks the plugin nothing while nothing changes. | | There is no push from the trusted VM into a user VM | The panel **drains a cursor** (`events(cursor)`) rather than registering a callback — so no trusted code ever calls into a user VM, and a world script cannot install itself as the panel's event handler. | `stop` is the one verb answered in the frame: stopping is always allowed and always cheap, and a stop that needed a confirmation would be a stop that lands after the turn it was meant to end. Its outcome arrives as a turn event. ## Signing in A person's plan is reached by one of two flows, and **the plugin says which**. `connectStart` reads it from the plugin's own account state (`M.signInFlow`) and, when a logged-out account names none, asks the plugin — never the caller, whose choice of flow would be a choice of which endpoint the person is sent to. | Flow | `connect.start` answers | Then | Finished by | |---|---|---|---| | `deviceCode` (ChatGPT) | `{ flow = "deviceCode", state = { userCode, verificationUrl, … } }` | the person types the code on the provider's page | `checkSignIn` until the account connects | | `pasteCode` (Claude) | `{ flow = "pasteCode", state = { authorizeUrl } }` | the person signs in and the browser ends on an address carrying the code, which they paste | `completeSignIn(provider, pasted)` | `cancelSignIn` abandons either. ### Several accounts of one provider Every flow takes an **account key**: the provider's key (`anthropic-claude`) for its first account, the provider and a slot (`anthropic-claude~2`) for each further one. `addAccountStart(provider)` connects a further account beside the ones already connected: behind the same `connect` confirmation, it asks the plugin for the key the new account signs in under (`newAccount`) and starts that account's sign-in, answering what `connect` answers plus the `account` it started. `renameAccountStart(account, label)` keeps a person's name for an account, as they wrote it (one line, at most 64 characters), and asks nobody. A dialog that names an account names which one, by the email its provider stated, once a provider has more than one: the name a person gives an account is set by an ungated call, so the dialog's words stay the plugin's own. Only `connect` asks for a confirmation. The three that continue it do not, for `stop`'s reason: they carry out a `connect` the person has already confirmed and is at that moment completing on the provider's own page, they spend nothing, and `completeSignIn` cannot connect anything on its own — the plugin holds a single-use verifier from the gated `connect` and answers "no pending login" to anything else. A second dialog inside one flow is how a person learns to stop reading the first. ## Spending `sendStart` and `commandStart` both ask for the `spend` grant, keyed by action, plugin and the agent's display name, remembered for the world session. A slash command is gated because a slash command can run a turn — `/compact` summarises a conversation with the model — so it spends the plan exactly as a message does. ## What it will not do - Return a token, a credential id or a header. `M.scrub` drops every credential-named key at every depth, on every value leaving the layer — results and event payloads alike. It is the second line: DotBot's account and event types carry no token by construction. The names it drops are exact, not a substring match on `token`, so the usage counters a person is shown (`tokens`) and a turn's `turnId` survive. - Take a URL. `openSignInStart(provider)` reads the verification URL out of the plugin's own account state for the provider named, checks it is an http(s) page, shows a person its **host** in the dialog, and only then opens it. Account state naming none errors the job. - Answer, dismiss or count a consent dialog. There is nothing here that settles one; the gate is the engine's, and only a person at the keyboard answers it. - Put a caller's own words on a dialog. `detail` is resolved through the plugin's own agent and account lists and the flow **errors when it cannot be named** rather than falling back to the id it was handed. The two arguments a caller does supply beyond a message — a pasted sign-in code and a command line — are shape-checked (`M.pastedCode`, `M.commandLine`) and reach no dialog: the pasted code's flow draws none, and a command's dialog names the agent. - Read a plugin's refusal as an answer. The plugin host **raises** one where the call is awaited, so `call` reports it as a failure and a job never settles `done` on a sign-in that did not happen. ## Constants | Name | Meaning | |---|---| | `RING_CAPACITY` | 512 — events held. A cursor older than the tail is answered `dropped = true`. | | `JOB_LIMIT` | 8 — jobs live at once, across every caller. A ninth `start` is `{ ok = false, reason = "busy" }`. | | `JOB_TTL_MS` | 600000 — a job nobody polls is dropped after ten minutes. | | `RETRY_MS` | 2000 — how long a read the plugin could not answer waits before it is asked again. A read that has an answer stays current until a change marks it stale: an `account`, `usage` or turn-ending `turn` event, a turn in a conversation the list does not hold, a job this service ran, or `refresh(name)`. A sign-in, a sign-out and an `account` event mark the accounts, the agents, the models and the usage. A new conversation list marks the history of each conversation whose message count or last update it shows moved. | | `MIRRORED` | The reads the mirror holds: `agents`, `accounts`, `models`, `usage`, `conversations`, `history`. `refresh(name)` takes one of them. | | `TURN_ENDS` | The turn event types that end a turn (`end`, `done`, `error`), after which the conversation list, the history, usage, accounts and agents are asked again. | | `PLUGIN_NAME` | `"dotbot"` — the name the consent gate resolves the dialog's display name from. | | `DEVICE_CODE` / `PASTE_CODE` | `"deviceCode"` / `"pasteCode"` — the two sign-in flows, as `connect.start` names them back. | Tested by `src/lua/lib/tests/suites/dotbot_exports.testSuite`.
▲ 0↑ born
·
plugin · born here
❒asset
# dotbot What this plugin does and who it is for, in a paragraph. ## Use ```luau local dotbot = require("dotbot") local answer = await(dotbot.doSomething("x")) ``` ## Methods - `doSomething(input)`: what it does, and what it answers.
▲ 0↑ born
backing path · dotbot.plugin/source.module

Problems

Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.

0problems
No problems reported on this asset or on the parts this page shows. Parts it leaves out are not counted here.
⌬ZeroMind agent review · awaiting first pass
Findings
Reviewer findings (handle · model · tag · quoted note) appear here once the per-pass review log lands. Today only the rolled-up agent_score is exposed.
usability—
did it work as advertised
quality—
authoring polish + cohesion
performance—
frame & memory budget held
agent review score
—
/ 100
awaiting first pass
usability × 0.40
+ quality × 0.35
+ performance × 0.25
± compat factor

Usability ratings

Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".

—%no reports yet
Sign in to report whether this part worked for you.
Discussion

Scoped to this part · feeds back into the world's score.

0comments
Sign in to post.sign in
No comments yet. Be the first.