fix: migrate tlon installs to tloncorp package

Switch new Tlon installs to @tloncorp/openclaw, add an on-load config migration for existing plugins.installs.tlon npm records, and document the rollout contract for the package rename. Preserve the tlon plugin/channel id and clear stale npm resolution metadata when rewriting old install specs so future plugin updates resolve cleanly.

Regeneration-Prompt: |
  Migrate the Tlon OpenClaw plugin package from @openclaw/tlon to @tloncorp/openclaw without breaking existing installs. Keep the plugin id and channel id as tlon, switch onboarding and docs for new installs to the new npm package, and add an automatic config migration for existing plugins.installs.tlon npm records that still reference the old package. When rewriting stored install specs, clear old npm resolution metadata such as integrity and resolvedSpec so the next plugin update does not compare the new package against the old package artifact. Also capture the migration contract in repo docs so the companion tloncorp package can preserve version continuity and manifest invariants.
This commit is contained in:
Josh Lehman
2026-03-10 14:23:10 -07:00
parent daf8afc954
commit 584f6bea5b
8 changed files with 189 additions and 3 deletions

View File

@@ -21,7 +21,7 @@ Tlon ships as a plugin and is not bundled with the core install.
Install via CLI (npm registry): Install via CLI (npm registry):
```bash ```bash
openclaw plugins install @openclaw/tlon openclaw plugins install @tloncorp/openclaw
``` ```
Local checkout (when running from a git repo): Local checkout (when running from a git repo):
@@ -32,6 +32,9 @@ openclaw plugins install ./extensions/tlon
Details: [Plugins](/tools/plugin) Details: [Plugins](/tools/plugin)
Existing installs from `@openclaw/tlon` are migrated to the new package name on config load so
future `openclaw plugins update` runs resolve from `@tloncorp/openclaw`.
## Setup ## Setup
1. Install the Tlon plugin. 1. Install the Tlon plugin.

View File

@@ -0,0 +1,73 @@
---
title: "Tlon Package Migration"
summary: "Plan for migrating the Tlon plugin package from @openclaw/tlon to @tloncorp/openclaw"
read_when:
- Updating Tlon onboarding/install metadata
- Migrating existing Tlon plugin installs
---
# Tlon Package Migration
This document captures the OpenClaw-side plan for migrating the Tlon plugin package from
`@openclaw/tlon` to `@tloncorp/openclaw` without breaking existing installs.
## Goals
- Keep the plugin id and channel id as `tlon`.
- Switch new onboarding and docs to install `@tloncorp/openclaw`.
- Auto-migrate existing `plugins.installs.tlon` records that still point at `@openclaw/tlon`.
- Preserve version continuity on the Tlon side so pinned or version-qualified specs can move cleanly.
- Keep local checkout installs (`./extensions/tlon`) working as before.
## Required invariants
The published `@tloncorp/openclaw` package must remain a drop-in replacement for the Tlon plugin.
- `package.json.name` should be `@tloncorp/openclaw`.
- `package.json.version` should continue the existing `2026.x.y` line instead of resetting to `0.x`.
- `openclaw.channel.id` must stay `tlon`.
- `openclaw.install.npmSpec` must be `@tloncorp/openclaw`.
- `openclaw.extensions` must point at the real published entrypoint.
- `openclaw.plugin.json.id` must stay `tlon`.
- `openclaw.plugin.json.channels` must contain `tlon`.
## OpenClaw migration behavior
On config load, OpenClaw should detect old Tlon npm install specs and rewrite them in place.
- Rewrite `plugins.installs.tlon.spec` from `@openclaw/tlon` to `@tloncorp/openclaw`.
- Rewrite version-qualified specs the same way, preserving the suffix after the package name.
- Apply this to any `plugins.installs.tlon` record that still references the old package name.
- Clear stale npm resolution metadata so the next `openclaw plugins update tlon` resolves against the
new package without comparing the old package integrity hash to the new package artifact.
Fields to clear when the spec is rewritten:
- `resolvedName`
- `resolvedVersion`
- `resolvedSpec`
- `integrity`
- `shasum`
- `resolvedAt`
Fields to preserve:
- `source`
- `installPath`
- `sourcePath`
- `version`
- `installedAt`
## Rollout
1. Publish `@tloncorp/openclaw` with the correct manifest and version continuity.
2. Update OpenClaw onboarding metadata and docs to point new installs at `@tloncorp/openclaw`.
3. Ship the config migration so existing installs start updating from the new package name.
4. Keep `@openclaw/tlon` available during the transition window.
5. Deprecate `@openclaw/tlon` after OpenClaw releases with the migration have had time to land.
## External follow-up in tloncorp/openclaw-tlon
- Publish the next `2026.x.y` version under `@tloncorp/openclaw`.
- Update the README to say the plugin is installed separately, not “included with OpenClaw”.
- Verify the published tarball contains the correct `openclaw.plugin.json`.

View File

@@ -923,7 +923,7 @@ its present in your workspace/managed skills locations.
Recommended packaging: Recommended packaging:
- Main package: `openclaw` (this repo) - Main package: `openclaw` (this repo)
- Plugins: separate npm packages under `@openclaw/*` (example: `@openclaw/voice-call`) - Plugins: separate npm packages, usually under `@openclaw/*` (examples: `@openclaw/voice-call`, `@tloncorp/openclaw`)
Publishing contract: Publishing contract:

View File

@@ -2,4 +2,6 @@
Tlon/Urbit channel plugin for OpenClaw. Supports DMs, group mentions, and thread replies. Tlon/Urbit channel plugin for OpenClaw. Supports DMs, group mentions, and thread replies.
Install from npm: `openclaw plugins install @tloncorp/openclaw`
Docs: https://docs.openclaw.ai/channels/tlon Docs: https://docs.openclaw.ai/channels/tlon

View File

@@ -24,7 +24,7 @@
"quickstartAllowFrom": true "quickstartAllowFrom": true
}, },
"install": { "install": {
"npmSpec": "@openclaw/tlon", "npmSpec": "@tloncorp/openclaw",
"localPath": "extensions/tlon", "localPath": "extensions/tlon",
"defaultChoice": "npm" "defaultChoice": "npm"
}, },

View File

@@ -1,6 +1,62 @@
import { describe, expect, it } from "vitest"; import { describe, expect, it } from "vitest";
import { migrateLegacyConfig } from "./legacy-migrate.js"; import { migrateLegacyConfig } from "./legacy-migrate.js";
import { WHISPER_BASE_AUDIO_MODEL } from "./legacy-migrate.test-helpers.js"; import { WHISPER_BASE_AUDIO_MODEL } from "./legacy-migrate.test-helpers.js";
import { findLegacyConfigIssues } from "./legacy.js";
describe("legacy migrate tlon plugin install spec", () => {
it("flags legacy @openclaw/tlon install specs for auto-migration", () => {
const issues = findLegacyConfigIssues({
plugins: {
installs: {
tlon: {
source: "npm",
spec: "@openclaw/tlon",
},
},
},
});
expect(issues).toContainEqual({
path: "plugins.installs.tlon.spec",
message:
"plugins.installs.tlon.spec moved from @openclaw/tlon to @tloncorp/openclaw (auto-migrated on load).",
});
});
it("rewrites old tlon install specs and clears stale npm metadata", () => {
const res = migrateLegacyConfig({
plugins: {
installs: {
tlon: {
source: "npm",
spec: "@openclaw/tlon@2026.2.21",
resolvedName: "@openclaw/tlon",
resolvedVersion: "2026.2.21",
resolvedSpec: "@openclaw/tlon@2026.2.21",
integrity: "sha512-old",
shasum: "old",
resolvedAt: "2026-03-01T00:00:00.000Z",
installPath: "/tmp/tlon",
version: "2026.2.21",
},
},
},
});
expect(res.changes).toContain(
"Moved plugins.installs.tlon.spec → @tloncorp/openclaw@2026.2.21 and cleared stale npm resolution metadata.",
);
expect(res.config?.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
expect(res.config?.plugins?.installs?.tlon?.version).toBe("2026.2.21");
expect(res.config?.plugins?.installs?.tlon?.installPath).toBe("/tmp/tlon");
expect(res.config?.plugins?.installs?.tlon?.resolvedName).toBeUndefined();
expect(res.config?.plugins?.installs?.tlon?.resolvedVersion).toBeUndefined();
expect(res.config?.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
expect(res.config?.plugins?.installs?.tlon?.integrity).toBeUndefined();
expect(res.config?.plugins?.installs?.tlon?.shasum).toBeUndefined();
expect(res.config?.plugins?.installs?.tlon?.resolvedAt).toBeUndefined();
});
});
describe("legacy migrate audio transcription", () => { describe("legacy migrate audio transcription", () => {
it("moves routing.transcribeAudio into tools.media.audio.models", () => { it("moves routing.transcribeAudio into tools.media.audio.models", () => {

View File

@@ -35,6 +35,16 @@ const AGENT_HEARTBEAT_KEYS = new Set([
const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]); const CHANNEL_HEARTBEAT_KEYS = new Set(["showOk", "showAlerts", "useIndicator"]);
function migrateLegacyTlonInstallSpec(spec: string): string | null {
if (spec === "@openclaw/tlon") {
return "@tloncorp/openclaw";
}
if (spec.startsWith("@openclaw/tlon@")) {
return `@tloncorp/openclaw${spec.slice("@openclaw/tlon".length)}`;
}
return null;
}
function splitLegacyHeartbeat(legacyHeartbeat: Record<string, unknown>): { function splitLegacyHeartbeat(legacyHeartbeat: Record<string, unknown>): {
agentHeartbeat: Record<string, unknown> | null; agentHeartbeat: Record<string, unknown> | null;
channelHeartbeat: Record<string, unknown> | null; channelHeartbeat: Record<string, unknown> | null;
@@ -97,6 +107,35 @@ function mergeLegacyIntoDefaults(params: {
// tools.alsoAllow legacy migration intentionally omitted (field not shipped in prod). // tools.alsoAllow legacy migration intentionally omitted (field not shipped in prod).
export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [ export const LEGACY_CONFIG_MIGRATIONS_PART_3: LegacyConfigMigration[] = [
{
id: "plugins.installs.tlon.spec->tloncorp",
describe: "Move Tlon plugin install records to @tloncorp/openclaw",
apply: (raw, changes) => {
const plugins = getRecord(raw.plugins);
const installs = getRecord(plugins?.installs);
const tlon = getRecord(installs?.tlon);
const spec = typeof tlon?.spec === "string" ? tlon.spec : null;
if (!tlon || !spec) {
return;
}
const nextSpec = migrateLegacyTlonInstallSpec(spec);
if (!nextSpec) {
return;
}
tlon.spec = nextSpec;
delete tlon.resolvedName;
delete tlon.resolvedVersion;
delete tlon.resolvedSpec;
delete tlon.integrity;
delete tlon.shasum;
delete tlon.resolvedAt;
changes.push(
`Moved plugins.installs.tlon.spec → ${nextSpec} and cleared stale npm resolution metadata.`,
);
},
},
{ {
// v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the // v2026.2.26 added a startup guard requiring gateway.controlUi.allowedOrigins (or the
// host-header fallback flag) for any non-loopback bind. The onboarding wizard was updated // host-header fallback flag) for any non-loopback bind. The onboarding wizard was updated

View File

@@ -46,6 +46,13 @@ function isLegacyGatewayBindHostAlias(value: unknown): boolean {
); );
} }
function isLegacyTlonInstallSpec(value: unknown): boolean {
if (typeof value !== "string") {
return false;
}
return value === "@openclaw/tlon" || value.startsWith("@openclaw/tlon@");
}
export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
{ {
path: ["whatsapp"], path: ["whatsapp"],
@@ -209,4 +216,10 @@ export const LEGACY_CONFIG_RULES: LegacyConfigRule[] = [
message: message:
"top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).", "top-level heartbeat is not a valid config path; use agents.defaults.heartbeat (cadence/target/model settings) or channels.defaults.heartbeat (showOk/showAlerts/useIndicator).",
}, },
{
path: ["plugins", "installs", "tlon", "spec"],
message:
"plugins.installs.tlon.spec moved from @openclaw/tlon to @tloncorp/openclaw (auto-migrated on load).",
match: (value) => isLegacyTlonInstallSpec(value),
},
]; ];