twinny¶
Twinny (catalog name twinny) is an open-source AI coding extension for VS Code (extension id
rjmacarthy.twinny); its chats live inside the editor's own SQLite state database, not in a file
of its own. Its repository also ships the twinny-node peer-to-peer GPU sharing CLI and the
twinny-server team gateway. The twinny parser reads the chats from the editor's state
database.
Source and evidence¶
twinnydotdev/twinny at
9339bd108e60781ec4d693fbec3f0bc7a0d7d3e6,
version 4.3.5, MIT
(package.json:32).
Open source, TypeScript. All claims from source. Citations name this commit unless they say
otherwise; facts new since 4.3.5 cite
0337b26
(version 4.3.15,
package.json:5).
How VS Code stores an extension's globalState was checked against
microsoft/vscode at the commit used by vscode.md,
and VS Code's extension storage and log-channel paths at
4d22f4f.
The repository holds three programs: the VS Code extension, the twinny-node
peer-to-peer GPU sharing CLI
(package.json:61-63)
and the twinny-server team gateway
(packages/twinny-server/package.json:28-30).
Observed¶
- No real install.
Source reviews¶
- 2026-10-21: twinnydotdev/twinny at
0337b26
(4.3.15), with microsoft/vscode at
4d22f4f
for the log file format: the parser plan now reads the standing grants, the team policy, the
reply's
changes, theagent-changes/records, the log channel and the review tab's repository; it corrected the team policy, which is kept inglobalState, not in memory only, and added the log's line format and rotation and the source-host finding. - 2026-10-19: twinnydotdev/twinny at
0337b26(version 4.3.15, committed 2026-10-16), against9339bd1(4.3.5). Between the two commits 160 files changed; every path in this document and every key and field the parser reads are still used. It added the records new since 4.3.5 (the per-reply file-change records, the log channel, the standing grants, the ChatGPT plan keys and the message fields of Record schema) and theuserrow for a step typed mid-reply. - 2026-10-17: twinnydotdev/twinny at
0337b26, for sync. It added the finding of Sync, relay and origin rows that Twinny records no sync state.
Collection¶
Per-user storage¶
Extension id rjmacarthy.twinny (publisher rjmacarthy, name twinny,
package.json:2,
33;
src/protocol/types.ts:118).
The editor's User/globalStorage/rjmacarthy.twinny/ directory is already collected under vscode;
what Twinny actually keeps is split three ways.
Layout is the same on Linux, macOS and Windows; only the editor's User directory moves (see
vscode.md). No environment overrides for ~/.twinny in the extension; the server's
paths can be changed in its config (auth.keysFile, auth.licenseFile, usage.dir,
recording.dir,
config.ts:312-330).
Scratch directories¶
Twinny has none. Agent mode works in the first workspace folder only
(src/extension/chat/index.ts:494-497),
and its one temporary file, a copy of the repository's index named twinny-index-<uuid> in the
system temporary directory, is deleted as soon as the tree is snapshotted
(src/extension/chat/command-snapshot.ts:62-80);
nothing for the catalog to collect or for discovery to drop.
VS Code globalState¶
Not files: chat history, the provider list and the active providers are
context.globalState keys
(src/common/constants/storage.ts:1-21):
twinny.conversations (every conversation, keyed by id), twinny.active-conversation,
twinny.inference-providers, twinny.active-chat-provider, -fim-provider,
-embeddings-provider, P2P device and trusted-peer lists, team share state
(src/extension/chat/conversation-history.ts:80-104;
src/extension/providers/store.ts:47-64).
VS Code persists an extension's global state as one JSON string in the profile state.vscdb
ItemTable, keyed by the extension id
(extHostMemento.ts:118;
extensionStorage.ts:159-176).
So Twinny's chats live in User/globalStorage/state.vscdb, row key = 'rjmacarthy.twinny', which
the vscode entry collects. Per-workspace UI state (chatMessage draft, contextItems,
selection, embeddingsUpdatedAt,
storage.ts:35-47;
src/extension/webview/base.ts:301-322)
goes to workspaceStorage/<id>/state.vscdb under the same key.
Extension global storage¶
globalStorage/rjmacarthy.twinny/ holds only:
twinny-providers.json, the provider list includingapiKey, written only when the settingtwinny.providerStorageLocationisfile(defaultglobalState) (store.ts:37-41, 141-171; src/common/constants/misc.ts:29; package.json:699-708).p2p-host.lockandteam-share.lock,{pid, since}window locks (src/extension/p2p/host.ts:40, 83-88; src/extension/team/share.ts:28, 86-90; src/extension/utils/window-lock.ts:26-52).
No chat, embedding or symmetry data is written there, except
agent-changes/ when no folder is open (Since 4.3.5).
Since 4.3.5¶
Since 4.3.5, at
0337b26:
workspaceStorage/<id>/rjmacarthy.twinny/agent-changes/<uuid>.json, one record per agent-mode reply that changed files, with the text of each file (up to 1 MiB) before the reply (src/extension/chat/index.ts:210-221; src/extension/chat/reply-changes.ts:25-47, 211-259), orglobalStorage/rjmacarthy.twinny/agent-changes/with no folder open; the record is in Per-reply file-change records. Nestedtwinnycatalog entries (<User dir>/workspaceStorage/*/rjmacarthy.twinnyfor each editor layout) claim the workspace copy from the editor'sUserdirectory; theworkspace.jsonandstate.vscdbbeside it stay the editor's (vscodeor the fork).- Standing permission grants and the current
twinny.selectedModel, as keys of the extension'sglobalStatevalue (therjmacarthy.twinnyrow ofglobalStorage/state.vscdb), and thetwinny.chatTools*editor settings in the usersettings.json(package.json:645-720); the keys and values are in Standing permission grants. Both files are collected as agentvscode. - The ChatGPT plan sign-in's profile,
twinny.chatgpt-plan.profile(clientId,subject,email,nameof the signed-in OpenAI account) andtwinny.chatgpt-plan.host-id, also in theglobalStatevalue (src/extension/chatgpt-plan/session.ts:11-13, 52-57, 397-402). - The Twinny log channel,
<editor logs>/<session>/window<n>/exthost/rjmacarthy.twinny/Twinny.log, one line per chat request and the prompts and replies at Debug level, and the copies it is rotated to,Twinny.log_1toTwinny.log_6(Twinny log channel); collected as agentvscodeunder the editor'slogsentries. - The pull request review tab's owner and repository, in the extension's per-workspace state (the
rjmacarthy.twinnyrow ofworkspaceStorage/<id>/state.vscdb); the keys are in Pull request review repository.
~/.twinny¶
~/.twinny from os.homedir() on every OS (no platform branching):
templates/*.hbs: editable prompt templates, defaults copied in on first run (src/index.ts:186; src/extension/templates/provider.ts:18, 68-80, 157).embeddings/<workspace name>/: a LanceDB tablechunksplusmanifest.jsonper workspace; the name isvscode.workspace.namewith characters outside[A-Za-z0-9_.-]replaced by_(src/extension/embeddings/index.ts:85-91; database.ts:9-11, 102-104; src/extension/utils.ts:369-376).node/identity.json(P2P key seed, mode 0600) andnode/trusted-peers.json, written bytwinny-node(src/node/config.ts:1-24, 51-55).server/, thetwinny-serverdata directory (src/gateway/config.ts:209-222):format.json,keys.json,license,invites.json,plugins.json,usage/,recordings/,audit/,plugins/(src/gateway/data-format.ts:18-21).usage/YYYY-MM-DD.jsonlper-request records (usage.ts:41, 83);audit/YYYY-MM.jsonladmin actions (audit.ts:3, 101-102; serve.ts:354);recordings/recordings.sqliteorrecordings/jsonl/YYYY-MM-DD.jsonl, full prompts and replies, off by default (src/gateway/recording/store.ts:1-11, 553-556; config.ts:216-222);plugins/<id>/plugin state (plugins/host.ts:492-494). The server's own configuration istwinny.gateway.jsonin the directory it was started from (src/gateway/init.ts:19, 150).
Credentials¶
- Provider API keys, in plain text: in
state.vscdb(rjmacarthy.twinny→twinny.inference-providers) or inglobalStorage/rjmacarthy.twinny/twinny-providers.json; the field is in Secrets. - Team gateway tokens: VS Code secret storage,
twinny.gateway-token.<providerId>(src/extension/providers/credentials.ts:14, 59-61); P2P identity and host seeds also secret storage (storage.ts:11-13; src/extension/p2p/runtime.ts:261-265; host.ts:336-340). These are encryptedsecret://rows instate.vscdb(see vscode.md). twinny.githubTokensetting, a GitHub PAT (personal access token), in the editorsettings.json(package.json:693-698): the token the pull request review sends to GitHub (Source host rows). It shares the file with every other editor setting, so it cannot be a secret glob.~/.twinny/node/identity.json: thetwinny-nodekey seed.- ChatGPT plan sign-in (new since 4.3.5): access, refresh and id tokens in VS Code secret storage
under
twinny.chatgpt-plan.credentials, an encryptedsecret://row; the account's email and subject in plainglobalState(Since 4.3.5) (src/extension/chatgpt-plan/session.ts:11-13, 59-66, 484). The profile is redacted by key (Secrets):state.vscdbalso holds the chats, so it cannot be a secret glob. twinny.chatToolsMcpServersin the usersettings.jsoncan carry tokens in each server'senv(the setting's own example is aGITHUB_TOKEN, package.json:684-720).- Exported provider lists: the "Export providers" command writes the providers,
apiKeyincluded, to a file the user picks, namedtwinny-providers.jsonby default (src/extension/providers/manager.ts:690-704); only the copy inglobalStorage/rjmacarthy.twinny/is in a known place. twinny-server:license(a licence token),keys.json(SHA-256 of client keys, not the keys, src/gateway/keys.ts:4-7),plugins/<id>/settings.jsonwith git clone tokens for the context plugin (src/gateway/plugins/context.ts:59-60, 214, 236-241), andplugins/<forge>/repos.jsonwith the forge plugins' saved access tokens (Source host rows). Backend API keys are read from environment variables named in the config (apiKeyEnv, config.ts:58, 237).
Exclusions¶
~/.twinny/embeddings/*/chunks.lance: the LanceDB vector table of source chunks; size grows with the indexed workspace. Keepmanifest.json.~/.twinny/server/plugins/*/repos/*/checkout(shallow git clones) and.../index(manifest.json,chunks.json,vectors.bin) (context.ts:271-273, 290-295, 336-338).
Project-local files¶
None written. Twinny reads each workspace's .gitignore to filter files
(src/extension/embeddings/indexer.ts:85-94)
and has no rules or instructions file of its own.
Where the project path is recorded¶
~/.twinny/embeddings/<workspace>/manifest.json:filesis keyed by the absolute path of every indexed file (database.ts:45-51; indexer.ts:123-133, 313). The workspace root is a common prefix of those keys, not a field.workspaceStorage/<id>/rjmacarthy.twinny/agent-changes/*.json(since 4.3.5): keyroot, the absolute workspace root agent mode worked in (src/extension/chat/reply-changes.ts:42-47, 161); a grep-able"root":"<path>"in one-line JSON.- Conversations carry no workspace path. The workspace link is the
workspaceStorage/<id>/workspace.jsonthat holds Twinny's per-workspace state.
Parsing¶
Transcript stores¶
<editor User dir>/globalStorage/state.vscdb,ItemTablerow whosekeyisrjmacarthy.twinny. Itsvalueis one JSON object holding everyglobalStatekey of the extension (extHostMemento.ts:118; extensionStorage.ts:159-176). Inside it,twinny.conversationsis an object of conversations keyed by id, andtwinny.active-conversationa copy of the open one (src/common/constants/storage.ts:1, 6; src/extension/chat/conversation-history.ts:80-104). One row holds all conversations. Home-relative globs:.config/*/User/globalStorage/state.vscdb,Library/Application Support/*/User/globalStorage/state.vscdb,AppData/Roaming/*/User/globalStorage/state.vscdb,.*-server*/data/User/globalStorage/state.vscdb, all collected as agentvscode.- Optional
twinny-serverrecordings (~/.twinny/server/recordings/), off by default, cover requests from every team member; see Other stores.
What survives a delete¶
Deleting a conversation removes its key from twinny.conversations and clears
twinny.active-conversation when it was open, and "clear all" writes {}
(conversation-history.ts:157-178;
at 4.3.5 the same code is at
conversation-history.ts:150-156
and
:167-170);
both touch only the extension's globalState row, and neither touches agent-changes/ or the log.
A per-prompt history file is not stated. What remains is the per-reply file-change records under the
workspace's agent-changes/ directory, the newest 50 kept, each with the project root, the time and
the text of every changed file before the reply, and the Twinny log channel's lines, which join a
conversation by time and text only and last until VS Code removes the log session. A twinny-server
gateway whose administrator turned on recording keeps its own copy of each request on the gateway
host; whether a delete reaches it is not stated, and nothing on the developer's machine says a
request was recorded. No deletion marker is stated. The analyzer reads the rjmacarthy.twinny row,
and the agent-changes/ records and the log channel as secondary sources, so a deleted
conversation's file changes, project root and log lines stay in the timeline; the gateway
recordings are not read (see Secondary sources).
Record schema¶
Conversation
(src/common/types.ts:127-135):
id (uuid v4,
src/webview/chat.tsx:494),
title, messages[], pinnedTitle?, updatedAt? (Unix ms, set on every save,
conversation-history.ts:97).
messages[] are ChatCompletionMessage: the OpenAI-style ChatCompletionMessageParam from
fluency.js (role, content) plus Twinny fields
(types.ts:50-76):
role:userorassistant(src/common/constants/misc.ts:8-11; user turns chat.tsx:496-503, replies src/extension/chat/generation.ts:151-157). The system prompt sits only in the extension's in-memory turn list (src/extension/chat/index.ts:192-195); saves come from the webview's message list (src/webview/hooks/useConversationHistory.ts:44).content: string. Reasoning models' output stays inline as<think>...</think>or<thinking>...</thinking>; the UI splits it (src/webview/utils.ts:116-121).id?,images?,prompt?(what was actually sent when the UI shows a shorter text),context?(workspace search hits used),toolNotes?(tool inputs and outputs carried to the next turn).meta?ReplyMeta(types.ts:104-125):model,provider,durationMs,completionTokens,promptTokens,contextWindow,stopped,withheld[](counts of credentials the secret shield masked).toolSteps?[]ToolStepView(types.ts:79-101):id,name,summary,args(string map),output,status(running|waiting|done|failed|skipped|stopped),command,approval.
Fields added between 4.3.5 and 4.3.15
(types.ts:76-165
at
0337b26): changes on an assistant message ({id, files[{path, status, added, removed, undone?,
noUndo?}]}, the view of the record in
Per-reply file-change records); toolSteps[].said (true on a
step whose name is said and whose summary is a message the user typed while the reply ran,
src/extension/chat/tool-steps.ts:101-104);
toolSteps[].mcpTool (server/tool, a call to a tool of an MCP server, Model Context Protocol, the
standard by which an external program offers tools to a model) and noAlways (the team gateway asks
for every command), both cleared when the step finishes
(:79-97);
meta.outOfSteps (the reply used every tool step twinny.chatToolsMaxSteps allows) and
meta.instructions (relative paths of the AGENTS.md files agent mode followed).
Joins and timestamps¶
Conversation id is the session. Tool steps are embedded in the assistant message that used them,
so there is no call/result join. No project path is stored with a conversation; the per-workspace
state (VS Code globalState) and the workspace.json beside it
(Where the project path is recorded) name the folder, and an
agent-changes/ record joins through changes.id
(Per-reply file-change records). The conversation list itself is
global.
There is no per-message timestamp; only the conversation's updatedAt.
Other stores¶
state.vscdb¶
state.vscdb is VS Code's SQLite ItemTable(key, value), with a -wal sidecar. The value is
UTF-8 JSON text. VS Code warns above 512 KB per extension
(extensionStorage.ts:47,
163-165),
so long histories are common in that one row.
Per-reply file-change records¶
Secondary, new since 4.3.5: when agent mode changes files, the extension keeps one JSON file per
reply under the workspace's extension storage, agent-changes/<uuid>.json
(src/extension/chat/index.ts:210-221;
src/extension/chat/reply-changes.ts:211-228).
The extension's workspace storage is <User dir>/workspaceStorage/<id>/rjmacarthy.twinny/, and the
global globalStorage/rjmacarthy.twinny/ when no folder is open
(extHostStoragePaths.ts:82-91).
A record is {id, root, createdAt, files[]}, each file {path, status, added, removed, undone?,
noUndo?, before, afterHash}
(reply-changes.ts:35-47):
root is the absolute workspace root, createdAt epoch ms at the end of the reply, status
modified, created or deleted, before the whole file text before the reply (files up to 1
MiB, text only,
:25-26)
and undone set when the user put it back
(:238-245).
The newest 50 records are kept and older ones deleted by modification time
(:28,
:247-259).
A record joins its conversation through the assistant message's changes.id
(src/common/types.ts:76-101),
and survives the conversation's deletion and "clear all", which touch only globalState. It is the
only Twinny record that names the project path.
Twinny log channel¶
The Twinny log channel (secondary, line-oriented): a VS Code log output channel
(src/common/logger.ts:93-97),
which VS Code writes to <extension host logs>/rjmacarthy.twinny/Twinny.log
(extHostOutput.ts:148-150)
under the editor's logs/ directory, collected as agent vscode. Each entry is one line
YYYY-MM-DD HH:MM:SS.mmm [<level>] <message>, the time in host local time with no zone and the
level one of trace, debug, info, warning and error
(fileLog.ts:62-101);
a message with line breaks continues on the following lines. When the file has passed 5 MB at a
flush, VS Code moves its content to Twinny.log_1, then _2 and so on up to _6, and starts
again at _1
(fileLog.ts:14,
41-44,
78-81). With twinny.enableLogging on
(the default,
package.json:633-638)
each chat request is one line Chat → <model> (<provider label>) · <n> messages, <n> chars and each
reply one line with its duration
(src/extension/chat/generation.ts:188-207,
:150-153);
at the channel's Debug level the messages and the reply are written as blocks, a title line
(Chat messages, Chat reply, FIM #<n> raw and others) followed by the text with every line
indented four spaces and cut at 8,000 characters
(logger.ts:46-47,
77-87,
140-144),
after the secret shield's redaction. Other Info lines record agent activity in the same channel,
among them Chat command: <command>, Chat command skipped: <command> and Chat created
<path>
(src/extension/chat/tool-sinks.ts:756-761;
src/extension/edit/service.ts:390). A line joins its conversation by time and text only (no
conversation id) and survives the conversation's deletion until VS Code removes the log session.
twinny-server recordings¶
twinny-server recordings, when enabled: SQLite recordings.sqlite (WAL) with recordings(id, ts,
at, key, route, alias, model, provider, outcome, ended, ms, prompt_tokens, completion_tokens,
request, response, preview, thread), or JSONL day files with the same RecordingRecord fields
(src/gateway/recording/store.ts:25-50,
336-355,
369-370,
207-211).
id is <ms, zero-padded to 14>-<hex>
(:99-100);
key is the client key's name, which attributes the request to a team member.
Standing permission grants¶
In the same rjmacarthy.twinny value, under keys prefixed twinny-global-context- (the prefix the
webview gives every global value): alwaysRunCommands (array of normalised commands the user chose
Always run for), alwaysRunMcpTools (array of server/tool names), autoRunCommands (boolean, the
composer's auto-run switch: every command runs unasked) and agentMode (boolean, tools on)
(src/common/constants/storage.ts:25-35;
src/extension/chat/index.ts:162-192,
:466-491).
The command twinny.forgetAlwaysRunCommands clears the first list
(src/index.ts:517-526).
Until the switches are first used, the editor settings twinny.chatTools (boolean),
twinny.chatToolsCommands (ask, allow, off) and twinny.chatToolsEdits (apply, review)
decide
(package.json:645-683);
those are in settings.json, which the vscode parser owns. An Always run choice leaves no mark on
the step itself, which ends done like any other
(tool-steps.ts:169-176),
so the list is the only record of it.
The policy a developer accepted when connecting to a team gateway is kept in the same value under
twinny.teamPolicy until they leave the team: {url, policy, providerIds, fetchedAt}, url the
gateway, providerIds the provider entries the connection added and fetchedAt a time string
(src/extension/providers/policy.ts:20-49;
src/common/team.ts:8-14).
policy holds teamOnly, lockDefaults, recording[], peers[], systemPrompt and agent,
whose off, commands (ask or off), edits (review), mcp (off) and blockedPaths[]
override the developer's own switches and Always run lists
(src/protocol/types.ts:44-100).
The extension also keeps the policy in force in memory
(src/common/team-policy.ts:79-89),
reloaded from the stored value.
Pull request review repository¶
The review tab keeps the GitHub owner and repository it last listed pull requests for in the
extension's per-workspace state, twinny-workspace-context-reviewOwner and
twinny-workspace-context-reviewRepo (strings) in the rjmacarthy.twinny row of
workspaceStorage/<id>/state.vscdb; it fills them from the workspace's GitHub remote when both
are empty, and lists that repository's open pull requests as soon as both are set
(src/webview/review.tsx:25-62;
src/common/constants/storage.ts:46-47;
src/extension/webview/base.ts:362-375).
The list comes from api.github.com each time and is not cached
(src/extension/review/service.ts:179-197).
Format versions¶
No version field on conversations. updatedAt is absent on conversations from older builds
(types.ts:133-134);
contextItems was previously contextFiles
(storage.ts:38).
Secrets¶
The same rjmacarthy.twinny JSON holds twinny.inference-providers and the
twinny.active-*-provider objects, each with apiKey
(types.ts:255-272).
Redact apiKey wherever it appears in the value. twinny.chatgpt-plan.profile names the signed-in
OpenAI account (email, subject, name) and is an identity value to redact; the parser never
reads it. Gateway tokens and P2P seeds are separate encrypted secret://
rows. In the editor's User/settings.json, redact twinny.githubToken and the env values of
each twinny.chatToolsMcpServers entry.
Parser plan¶
Select state.vscdb (and -wal) under any User/globalStorage/, read the
row key = 'rjmacarthy.twinny', parse JSON, iterate
twinny.conversations. Attribute rows to twinny, not vscode. Also select
User/workspaceStorage/<id>/state.vscdb (the same row, for
Source host rows),
User/workspaceStorage/<id>/rjmacarthy.twinny/agent-changes/*.json and
User/globalStorage/rjmacarthy.twinny/agent-changes/*.json, and
logs/.../rjmacarthy.twinny/Twinny.log with its rotated Twinny.log_<n> copies
(Past-use rows and secondary sources).
| Column | Source |
|---|---|
| timestamp_utc | the conversation's updatedAt (ms) on every row of it; a record's createdAt; the log line's time; the file's modification time for standing and source-host rows |
| session_id | conversation id; empty for the other sources |
| project_path | empty; an agent-changes/ record's root |
| git_branch | empty |
| turn_type | role; <think> block → thinking; each toolSteps[] → tool_use plus tool_result, except a said: true step → user |
| model | meta.model (assistant) |
| tool_name, tool_use_id | toolSteps[].name, toolSteps[].id |
| text | content with think block removed; tool_use: command or args JSON; tool_result: output; a said step: summary |
| source_line | 0 (one SQLite row or JSON file); the log entry's first line number |
Model and permission rows are all system
(issue 96 and
issue 97).
Permission rows¶
permission decision: <status>, withapproval=<approval>when the step records it, after thetool_resultrow of atoolSteps[]entry whosestatusiswaitingorskipped, with the step'sidandname.approval(commandorchange) is what the user is asked to approve, not the answer (types.ts:87-92). A step that asks setsstatuswaitingwithcommandandapproval; the user's Run or Skip returns it torunningand clears both, a Skip (or a stop that answers every waiting step with Skip) records the step as skipped, andfinishthen saves it asstatusskipped; a step still running or waiting when the reply ends is savedstopped(src/extension/chat/tool-steps.ts:76-93, 131-176). Sowaitingis a prompt saved before it was answered,skippedis a step the user (or a stop) did not let run, and an approved step is indistinguishable from one that needed no approval: both enddoneorfailed. Astoppedstep may or may not have been waiting, so it gets no decision row.- Standing grants and the team policy
(Standing permission grants), source-derived; not observed on any
real install. Each is a
systemrow with an emptysession_id, at the modification time ofstate.vscdb, its text ending(from state.vscdb rjmacarthy.twinny, file mtime):permission mode: agentMode=<true|false>,permission setting: autoRunCommands=<true|false>, onepermission setting: alwaysRunCommands=<command>per kept command and onepermission setting: alwaysRunMcpTools=<server/tool>per kept tool, then onepermission setting: twinny.teamPolicy.<dotted key>=<value>per leaf oftwinny.teamPolicy(url,policy.teamOnly,policy.agent.commands, ...,fetchedAt), a list giving one row per entry. The value holds only the current switches, soagentModeis one row for the state at the file's time, not a row per change. The editor settingstwinny.chatTools*are insettings.json, which thevscodeparser owns, and are not read here. node/trusted-peers.json, thetwinny-nodetrusted-peer list named in Per-user storage, is not read: its shape is not recorded.
Model rows¶
model: <id> at the first assistant message of a conversation whose meta.model is set and
model: <old> -> <new> on every later assistant message with a different one, before that
message's rows. The model is recorded per reply, so a change inside a conversation shows. That
value is the provider entry's configured modelName, set before the request
(generation.ts:112),
not a model the server reports, and meta.provider is overwritten with the backend's name when a
team gateway routes the request
(:123-126),
so for a team pool it names the teammate's machine that answered. The current selection is also
kept as twinny-global-context-twinny.selectedModel and in twinny.active-chat-provider
(src/extension/providers/store.ts:92-100);
neither is history.
Past-use rows and secondary sources¶
The agent-changes/ records (Per-reply file-change records) are
secondary for the reply they belong to, but the only source of its file changes and project path;
the log channel (Twinny log channel) is secondary and line-oriented. Both
are read on every run and marked secondary, source-derived; not observed on any real install.
- Each assistant message's
changesgives onesystemrow per file,file changed: <status> <path> +<added> -<removed> reply=<changes.id>, withundone=trueandnoUndo=truewhen the entry sets them, after the message's tool steps, in the conversation. - Each
agent-changes/record gives the same row per file, withproject_paththe record'sroot, the time itscreatedAtand an emptysession_id, since the record names no conversation;reply=<id>is the join to the conversation'schanges.id. The conversation records neither the project nor the reply's time, so deduplication cannot pair the two copies and both are shown.beforeandafterHashare never emitted. - Each entry of
Twinny.logand its rotated copies gives onesystemrow,log <level>: <first line> (from <file name>, log line time, host-local), followed on the next lines by the entry's continuation lines with the four-space block indent removed, with an emptysession_idandsource_linethe entry's first line. The log time has no zone and is emitted as if it were UTC. A log line joins its conversation by time and text only; every entry is kept, the Debug blocks' prompts and replies among them, since a deleted conversation's text may survive nowhere else.
Sync, relay and origin rows¶
Twinny records no sync, relay or origin state. Conversations are written and deleted only in the
extension's globalState
(conversation-history.ts:100-110,
158-178),
and the extension never calls setKeysForSync, the call that would let VS Code's Settings Sync (the
editor's own upload of settings and extension state to the user's Microsoft or GitHub account) carry
a globalState key to other devices
(vscode.d.ts:8445-8457;
extHostMemento.ts:113-114);
the P2P host says so in its own words, "global state is not synced"
(src/extension/p2p/host.ts:5-8).
The features whose names suggest sharing move compute, not chats: team share and P2P (peer-to-peer)
hosting lend this machine's local model server to a team gateway or to paired devices
(src/extension/team/share.ts:1-9;
src/extension/team/sharer.ts:1-9;
host.ts:1-3),
and their state keys twinny.teamShare, twinny.teamPluginsSeen and twinny.teamNudge hold the
share switch and server, the plugin ids already announced and a first-run notice flag
(src/common/constants/storage.ts:18-23);
a team-pool reply names the teammate's machine only in meta.provider (Model rows).
The OpenAI Responses adapters, including the ChatGPT plan sign-in, send store: false, so the
provider keeps no server-side conversation
(src/extension/inference/adapters/openai-responses.ts:54;
src/extension/inference/adapters/chatgpt-plan.ts:192).
Searched sync, upload, share, publish, export, telemetry, analytics, sentry,
posthog, record and CONVERSATION_STORAGE_KEY in src/extension, src/common and
src/webview; no telemetry client was found.
The one server copy is the team's own: when chat runs through a twinny-server gateway, the request
is the model call, and the gateway keeps it only if its administrator turns on recording
(recording.chat, default false, and only with the licence's recording feature,
src/gateway/config.ts:147-148,
216-223),
grouped by a thread id the gateway derives from the key name and the first user message, not one
the extension sends
(src/gateway/recording/store.ts:125-135).
The extension sends only the workspace name in X-Twinny-Workspace
(src/protocol/client.ts:83,
252),
and nothing on the developer's machine says a request was recorded, so the parser yields no sync
row; the gateway host's recordings/ directory is the place to look
(Other stores).
Scratch directory rows¶
None: Twinny has no scratch directory (Scratch directories).
Deletion rows¶
What a delete removes and leaves is in What survives a delete; the plan names no deletion row.
Bad-line rows¶
The rjmacarthy.twinny value and each agent-changes/ record are single JSON values: one that does
not decode is one whole-file parser: row (parser: agent-changes record is not valid JSON
(<reason>) for a record). The log channel is line-oriented: a line that is neither an entry
header nor a continuation of an open entry (indented or empty) is a bad line,
parser: unparseable line (no log line header): <line>, and the entries before and after it
are still yielded; a continuation that follows a bad line is a bad line too, since it has no entry
to join.
Fixtures¶
Fixture, the value of the rjmacarthy.twinny row:
{"twinny.conversations":{"7d1c2b3a-1111-4000-8000-000000000001":{"id":"7d1c2b3a-1111-4000-8000-000000000001","title":"List the tests","updatedAt":1759999205000,"messages":[{"role":"user","content":"which tests fail?"},{"role":"assistant","content":"<think>run the suite</think>Two tests fail.","meta":{"model":"qwen2.5-coder:7b","provider":"Ollama","durationMs":5200},"toolSteps":[{"id":"step-1","name":"run_command","summary":"ran `npm test`","args":{"command":"npm test"},"output":"2 failing","status":"done"}]}]}},"twinny.active-conversation":{"id":"7d1c2b3a-1111-4000-8000-000000000001"},"twinny.inference-providers":{"p1":{"id":"p1","label":"Ollama","provider":"ollama","type":"chat","modelName":"qwen2.5-coder:7b","apiHostname":"localhost","apiPort":11434,"apiKey":"REDACTME"}}}
Since 4.3.5, keys of the same value (standing grants and a team policy) and an assistant message
with changes, from the types cited in Standing permission grants
and Record schema:
{"twinny-global-context-agentMode":true,"twinny-global-context-autoRunCommands":false,"twinny-global-context-alwaysRunCommands":["npm test"],"twinny-global-context-alwaysRunMcpTools":["github/list_issues"],"twinny.teamPolicy":{"url":"https://gateway.example.invalid","policy":{"teamOnly":true,"agent":{"commands":"ask","blockedPaths":[".env"]}},"providerIds":["team-0a1b-chat"],"fetchedAt":"2026-10-03T09:00:00.000Z"}}
{"role":"assistant","content":"Fixed both.","meta":{"model":"qwen2.5-coder:7b"},"changes":{"id":"5f0e9d8c-2222-4000-8000-000000000001","files":[{"path":"src/a.ts","status":"modified","added":3,"removed":1}]}}
An agent-changes/<id>.json record
(Per-reply file-change records):
{"id":"5f0e9d8c-2222-4000-8000-000000000001","root":"/srv/proj","createdAt":1791029098000,"files":[{"path":"src/a.ts","status":"modified","added":3,"removed":1,"undone":true,"before":"export const a = 1\n","afterHash":"<sha256 hex>"}]}
Twinny.log entries (Twinny log channel):
2026-10-03 14:00:01.250 [info] Chat → qwen2.5-coder:7b (Ollama) · 2 messages, 41 chars
2026-10-03 14:00:01.251 [debug] Chat messages
[user]
which tests fail?
2026-10-03 14:00:06.400 [info] Chat ← 5.1s · 15 chars
The workspace state value of the review tab (Pull request review repository):
{"twinny-workspace-context-reviewOwner":"acme","twinny-workspace-context-reviewRepo":"widgets"}
Steps typed mid-reply¶
A toolSteps[] entry with said: true (since 4.3.5; name said, id said.<id>, the message
in summary, status done) is a message the user sent while the reply ran, and the parser emits
it as a user row with the summary as its text, in its place among the reply's steps, with no
tool_name, tool_use_id or model
(src/extension/chat/tool-steps.ts:101-104,
src/common/types.ts:129-133);
before the fix for issue 171 it was a
tool_use named said and an empty tool_result. changes is read
(Past-use rows and secondary sources);
mcpTool, meta.outOfSteps and meta.instructions are not read. The said step layout is not
observed on any real install, source-derived (twinnydotdev/twinny at
0337b26).
Source host rows¶
The extension connects to GitHub only for its pull request review, with the personal access token
of the twinny.githubToken editor setting (Credentials) sent as a bearer token,
or none
(src/extension/review/service.ts:154-177).
No account login is stored: the extension never asks GitHub who the token is. The repository the
review tab last listed is in the workspace state
(Pull request review repository); the pull request list is
fetched each time, and a review becomes an ordinary conversation whose first user message begins
**Code review: PR #<number> <title>**
(:199-218,
:303;
src/extension/review/diff.ts:255-260).
The parser yields source host repository: https://github.com/<owner>/<repo> type=review (from
workspaceStorage state.vscdb rjmacarthy.twinny, file mtime) when both keys are set, a system row
with an empty session_id at the modification time of that state.vscdb; source-derived; not
observed on any real install. It never reads settings.json.
On a twinny-server gateway host, the GitHub, GitLab, Gitea and Bitbucket plugins
(src/gateway/plugins/index.ts:18-21)
keep each forge's watched repositories and saved access tokens in
~/.twinny/server/plugins/<id>/repos.json, {version, repos[], tokens[], settings}, a repository
{id, fullName, auth, tokenId, addedAt, addedBy, autoReview, autoPost, autoTriage} and a token
{id, token, label, login, addedAt, addedBy, replacedAt}, login being the account the host said
the token belongs to
(src/gateway/plugins/forge.ts:109-143,
518-526;
src/gateway/plugins/pulls.ts:33-35,
165).
The collector flags the file as a secret (.twinny/server/plugins/*/repos.json). The parser does
not read it: a self-hosted forge's address is in the plugin's settings, which this review did
not trace, so the source host: and source host repository: rows for it are left for the
gateway's own round.
Confidence¶
- High (source): the extension id, the
globalStatekey names, the provider file name and setting, the~/.twinnysubpaths, the gateway data directory layout, the secret storage keys, the row key, theConversationand message fields, the roles and theupdatedAtunit. - High (VS Code source):
globalStatestored instate.vscdbunder the extension id. - Medium:
chunks.lanceas the on-disk name of the LanceDB table (LanceDB's naming convention, not visible in Twinny's source);contentis always a string in saved chats (thefluency.jstype also allows part arrays; Twinny writes strings at the cited sites). - Not determined: sizes of a real index; whether
invites.jsonholds reusable secrets; how the JetBrains or other editors (Twinny is VS Code only at this commit) would differ; howtoolSteps[].argsnames the command for each tool; whether other editors running the extension (VSCodium, Cursor) store it identically (they share the VS Code storage code, so likely).