turnstonelabs/turnstone/import-conversation-history

Claude Code and agent skill by turnstonelabs/turnstone

Trust tier 3
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Use this skill when the user wants to import or migrate conversation history from another LLM chat or coding tool (e.g. ChatGPT, Claude.ai, Cursor, Copilot Chat, Aider, Gemini, a custom JSON export) into Turnstone. The skill teaches Turnstone's destination contracts — workstream identity, the OpenAI-shaped message rows, tool-call/result pairing, provider-fidelity blobs, attachments, and archive-vs-resumable choice — so the agent can map any source format onto them. Trigger phrases: "import my chats", "migrate this transcript into Turnstone", "bring my Claude.ai history over", "load this export as a workstream".

Install

npx vskill@latest install turnstonelabs/turnstone/import-conversation-history

SKILL.md

Importing Conversation History into Turnstone

Overview

Source formats vary; the destination does not. Your job is to translate whatever the user hands you (JSON dump, ZIP export, scraped HTML, screenshot OCR, raw transcript) into Turnstone's internal shape: one workstream row plus an ordered sequence of conversation rows in OpenAI message format. This skill documents the destination so you can write a correct mapper for any source.

Two questions to settle with the user before writing anything:

  1. Archive or resumable? An archive is left closed and is read-only history. A resumable import is also kept closed and unloaded while rows are written, then explicitly opened after validation; this only works cleanly when the source LLM matches a Turnstone-supported provider/model and tool definitions still resolve.
  2. One workstream per source thread, or merge? Default to one-to-one unless the user explicitly asks to merge.

Default to archive when in doubt — resuming a foreign transcript with mismatched tool schemas or stale provider signatures will fail at the next turn.

Turnstone Data Model (the destination)

Two tables carry the conversation:

workstreams (one row per imported thread)
ColumnRequiredNotes
ws_idyes32-char lowercase hex. Auto-generate with secrets.token_hex(16) if you don't already have one. The router hashes the full ID — see "Identity & Routing" below.
nameyesShort title. Pull from source thread title; fall back to first ~60 chars of first user message.
stateyesRegister as "closed" while importing. Leave it closed for an archive; explicitly open it after commit for a resumable import. Never set "running" or "creating" directly.
kindyes"interactive" for normal threads. Do NOT use "coordinator" for imports — that's reserved for cluster-spawned coordinator workstreams.
parent_ws_idnoLeave NULL. Only set if you're importing a coordinator-spawned subtree and re-parenting it; rare.
user_idyesOwner. Must exist in users; importer must know which Turnstone user owns the imported history.
node_idnoNullable creation-time placement hint. It is not the routing key or the owner (the owner lease is) and may become stale after membership changes. Let a routed create stamp it; a direct shared-storage import may leave it NULL.
aliasnoHuman-typeable short name. Optional; must be unique cluster-wide if set.
titlenoAuto-titled later by the LLM; safe to leave NULL on import.
skill_id, skill_versionyesDefault "" and 0 unless the source thread was scoped to a Turnstone skill.
created, updatedyesISO8601 strings. Use the source's first/last message timestamps when available.
conversations (many rows per thread, ordered by id/timestamp)
ColumnNotes
ws_idThe workstream this row belongs to.
timestampISO8601 string. Preserve source timestamps; fall back to monotonically increasing values if unknown. Order is canonical via `id` (autoincrement), not `timestamp` — but always insert in conversational order so both agree.
roleOne of system, user, assistant, tool, developer. See role mapping below.
contentText. May be NULL for assistant rows that are only tool calls.
tool_nameSet on role="tool" rows (the tool whose result this is). NULL otherwise.
tool_call_idSet on role="tool" rows (matches the assistant row's tool_calls[].id). NULL otherwise.
tool_callsJSON-encoded list, on role="assistant" rows that issued tool calls. OpenAI shape — see "Tool Calls" below.
provider_dataJSON blob preserving provider-native content blocks (Anthropic signature, Gemini thought_signature, etc.). Optional; only matters for resumable imports against the same provider. Skip for archives.

The internal format is OpenAI-shaped, even when the source was Anthropic or Gemini. Providers translate at their own API boundary; storage stays uniform.

Identity & Routing (ws_id)

  • ws_id is 32-char lowercase hex (i.e. secrets.token_hex(16)).
  • Ordinary placement is rendezvous (Highest Random Weight, HRW) selection over

the full `ws_id` and the current live server set. For each node, Turnstone computes 32-bit FNV-1a over the node ID, a NUL separator, and the full workstream ID; it then applies the node weight and selects the highest score. A live per-workstream override takes precedence.

  • The live set comes from recent services heartbeats. Placement can therefore

change when nodes join, leave, change weight, or an override changes. There is no stable prefix-derived placement to pre-compute or persist.

  • workstreams.node_id is stamped at creation and is not updated as HRW

placement changes. It is display metadata: cleanup keys on the owner lease, and the console router prefers the node holding a live lease before ordinary placement.

  • For multi-node imports, create through the console routing proxy when the

lifecycle must be published, or write the history once through the cluster's configured shared storage backend. Never partition rows across node-local databases by ID prefix or by a one-time HRW result: a later membership change can route the same full ID to another node.

  • For single-node imports, HRW placement is degenerate; any valid ws_id works.
  • Do not reuse the source platform's IDs as `ws_id` unless they happen to be 32-char hex. Generate fresh; if you need the old ID for traceability, store it in workstream_config under a key like import.source_id.

Recommended Import Path

Three options, in order of preference:

1. Quiesced storage import (recommended for full history)

Use the current turnstone.core.storage.StorageBackend protocol against the same shared backend as the cluster. The destination must remain absent from all in-memory session managers while rows are changing: a loaded ChatSession holds its own trajectory and will not observe conversation rows inserted behind it.

The safe sequence is:

  1. Normalize and validate the complete source transcript before writing.
  2. Call register_workstream(..., state="closed") and require a True return;

False means the caller-selected ID already exists, so abort rather than appending to an unrelated workstream.

  1. Insert the ordered conversation rows and attachment references.
  2. Load the saved rows back and run the validation checklist below.
  3. Leave an archive closed. For a resumable import, only now invoke the normal

POST /v1/api/workstreams/{ws_id}/open endpoint on the currently routed node so the session hydrates from the complete transcript.

Do not create the destination through the web/SDK create endpoint before a direct bulk import. Create publishes an empty live session. If that already happened, close the workstream and confirm the manager-authoritative live probe returns false before writing, then explicitly open it again after validation. Storage enforces this: while any server holds the workstream's owner lease, the import's writes are refused with WorkstreamLeaseHeldError.

For attachment-free history, save_messages_bulk(rows) is the canonical single-transaction insert primitive and bypasses the LLM round-trip entirely. New attachment bytes require the per-row path described under Attachments.

python
from turnstone.core.storage import get_storage # initialized by the host/import entry point
storage = get_storage()
inserted = storage.register_workstream(
ws_id=ws_id,
user_id=user_id,
name=name,
state="closed",
kind="interactive",
...
)
if not inserted:
raise RuntimeError(f"destination already exists: {ws_id}")
storage.save_messages_bulk([
{"ws_id": ws_id, "role": "user", "content": "Hello"},
{"ws_id": ws_id, "role": "assistant", "content": "Hi! What can I help with?"},
{"ws_id": ws_id, "role": "assistant", "content": None,
"tool_calls": json.dumps([{"id": "call_1", "type": "function",
"function": {"name": "search", "arguments": "{\"q\":\"x\"}"}}])},
{"ws_id": ws_id, "role": "tool", "tool_name": "search", "tool_call_id": "call_1",
"content": "result text"},
# ...
])

save_messages_bulk handles timestamp and the workstream's updated column internally, so you don't need to compute them per row. Verify the exact register_workstream and message signatures in turnstone/core/storage/_protocol.py; the Storage protocol, not the physical table layout, is the source of truth.

Multi-node note: this path assumes get_storage() is connected to the cluster's shared backend. Do not open a node-local database selected from the current HRW result, and do not pre-create a live session through the console routing proxy. After the shared-storage import commits, resolve the current route and open the closed workstream on that node. Any stored node_id describes creation-time placement, not a permanent shard that should receive a separate copy.

2. SDK create_workstream(resume_ws=...) (when the source is already a Turnstone workstream)

Only useful for Turnstone → Turnstone re-parenting. Not relevant for foreign sources.

3. SDK create_workstream(initial_message=...) + send() per turn (last resort)

Only fits archives where the source had no tool calls and you don't care about preserving assistant turns verbatim. Each send() triggers a real LLM round-trip, which is expensive and rewrites assistant content. Don't use this for full history.

Role Mapping

Common source-role conventions and how they map to Turnstone:

Source roleTurnstone roleNotes
user, humanuserDirect map.
assistant, ai, model, botassistantDirect map.
systemsystemPreserve only if it's content the user wrote (custom instructions). Drop boilerplate provider preambles — Turnstone composes its own system message.
developer (OpenAI o-series)developerPreserve.
tool, function, tool_resulttoolMust carry tool_name and tool_call_id matching the prior assistant row's tool_calls[].id.
tool_use (Anthropic)assistant with tool_callsAnthropic emits tool calls inside an assistant message; flatten to OpenAI shape.
human_feedback, revisionuserTreat as a follow-up user turn.

Tool Calls (the most error-prone part)

Turnstone stores tool calls in OpenAI's nested-function shape on the assistant row, and matches them with role="tool" result rows by tool_call_id.

Assistant row with tool calls
json
{
"role": "assistant",
"content": null,
"tool_calls": [
{
"id": "call_abc123",
"type": "function",
"function": {
"name": "search_web",
"arguments": "{\"query\":\"turnstone import\"}"
}
}
]
}

tool_calls[].function.arguments is a JSON-encoded string, not an object. Source formats commonly get this wrong — Anthropic stores arguments as a parsed object, Gemini as a struct. Always re-serialize to a string.

Tool result row
json
{
"role": "tool",
"tool_name": "search_web",
"tool_call_id": "call_abc123",
"content": "..."
}

Pairing rules:

  • Every assistant tool_calls[].id MUST be followed by exactly one role="tool" row with the matching tool_call_id, before the next user/assistant turn.
  • If the source dropped the tool result (cut-off transcript), insert a synthetic role="tool" row with content="[tool result missing in source]" to keep the chain valid. An assistant row with an unanswered tool_calls[].id will break replay and any LLM round-trip.
  • Multi-tool assistant turns: one role="tool" row per call, in any order, all before the next non-tool row.
Tool ID generation

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 95 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
922
Repository forks
78
7-day trend score
34

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/turnstonelabs/turnstone/import-conversation-history/badge)](https://verified-skill.com/skills/turnstonelabs/turnstone/import-conversation-history)

Something wrong with this skill or its page? Tell us.