@inetafrica/open-claudia 2.15.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +30 -4
- package/CHANGELOG.md +12 -0
- package/README.md +87 -66
- package/bin/agent.js +44 -18
- package/bin/cli.js +30 -28
- package/bin/dream.js +5 -11
- package/bin/loopback-client.js +29 -5
- package/bin/schedule.js +19 -5
- package/bin/tool.js +23 -5
- package/bot-agent.js +5 -2350
- package/bot.js +66 -3
- package/channels/telegram/adapter.js +6 -1
- package/channels/voice/adapter.js +1 -0
- package/channels/voice/manage.js +43 -5
- package/config-dir.js +3 -11
- package/core/actions.js +149 -40
- package/core/approvals.js +136 -29
- package/core/auth-flow.js +103 -19
- package/core/config-dir.js +19 -0
- package/core/config.js +115 -69
- package/core/doctor.js +111 -57
- package/core/dream.js +219 -62
- package/core/enforcer.js +0 -0
- package/core/entities.js +2 -1
- package/core/fsutil.js +21 -4
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +42 -6
- package/core/memory-mutation-queue.js +241 -0
- package/core/migration-backup.js +1060 -0
- package/core/pack-review.js +295 -88
- package/core/packs.js +4 -3
- package/core/persona.js +2 -1
- package/core/process-tree.js +19 -2
- package/core/provider-migration.js +39 -0
- package/core/provider-status.js +80 -0
- package/core/providers/claude-events.js +117 -0
- package/core/providers/claude-hook.js +114 -0
- package/core/providers/claude.js +296 -0
- package/core/providers/codex-events.js +121 -0
- package/core/providers/codex-hook.js +280 -0
- package/core/providers/codex.js +286 -0
- package/core/providers/contract.js +153 -0
- package/core/providers/env.js +148 -0
- package/core/providers/events.js +170 -0
- package/core/providers/hook-command.js +22 -0
- package/core/providers/index.js +240 -0
- package/core/providers/utility-policy.js +269 -0
- package/core/recall/classic.js +2 -2
- package/core/recall/discoverer.js +71 -22
- package/core/recall/metrics.js +27 -1
- package/core/recall/tuning.js +4 -1
- package/core/recall/warm-walker.js +151 -108
- package/core/recall-filter.js +86 -61
- package/core/router.js +79 -7
- package/core/run-context.js +185 -0
- package/core/runner.js +1414 -1290
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1084 -97
- package/core/subagent.js +72 -98
- package/core/system-prompt.js +462 -279
- package/core/tool-guard.js +73 -39
- package/core/tools.js +22 -5
- package/core/transcripts.js +30 -1
- package/core/usage-log.js +19 -4
- package/core/utility-agent.js +654 -0
- package/core/web-auth.js +29 -13
- package/docs/CHANNEL_DESIGN.md +181 -0
- package/docs/MULTI_CHANNEL_PLAN.md +254 -0
- package/docs/PROVIDER_MIGRATION.md +67 -0
- package/health.js +50 -39
- package/package.json +48 -4
- package/setup.js +198 -38
- package/soul.md +39 -0
- package/test-ability-extraction.js +5 -0
- package/test-approval-async.js +63 -4
- package/test-cursor-removal.js +334 -0
- package/test-enforcer.js +70 -27
- package/test-fixtures/current-provider-parity.json +132 -0
- package/test-fixtures/fake-agent-cli.js +477 -0
- package/test-fixtures/migrations/claude-only/jobs.json +3 -0
- package/test-fixtures/migrations/claude-only/sessions.json +5 -0
- package/test-fixtures/migrations/claude-only/state.json +5 -0
- package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
- package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
- package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
- package/test-fixtures/migrations/cursor-selected/state.json +6 -0
- package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
- package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
- package/test-fixtures/migrations/dual-provider/state.json +10 -0
- package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
- package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
- package/test-fixtures/migrations/malformed-partial/state.json +1 -0
- package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
- package/test-fixtures/migrations/missing-project/jobs.json +3 -0
- package/test-fixtures/migrations/missing-project/sessions.json +3 -0
- package/test-fixtures/migrations/missing-project/state.json +4 -0
- package/test-fixtures/migrations/multi-user/jobs.json +4 -0
- package/test-fixtures/migrations/multi-user/sessions.json +4 -0
- package/test-fixtures/migrations/multi-user/state.json +6 -0
- package/test-fixtures/provider-event-samples.json +17 -0
- package/test-learning-e2e.js +5 -0
- package/test-memory-mutation-queue.js +191 -0
- package/test-provider-boot-matrix.js +399 -0
- package/test-provider-bootstrap.js +158 -0
- package/test-provider-capabilities.js +232 -0
- package/test-provider-claude.js +206 -0
- package/test-provider-codex.js +228 -0
- package/test-provider-compaction.js +371 -0
- package/test-provider-core-prompt.js +264 -0
- package/test-provider-coupling-audit.js +90 -0
- package/test-provider-dream.js +312 -0
- package/test-provider-enforcer.js +252 -0
- package/test-provider-env-isolation.js +150 -0
- package/test-provider-events.js +141 -0
- package/test-provider-fixture.js +332 -0
- package/test-provider-gateway.js +508 -0
- package/test-provider-language.js +89 -0
- package/test-provider-migration-backup.js +424 -0
- package/test-provider-pack-review.js +436 -0
- package/test-provider-parity-e2e.js +537 -0
- package/test-provider-prompt-parity.js +89 -0
- package/test-provider-recall.js +271 -0
- package/test-provider-registry.js +251 -0
- package/test-provider-resume-parity.js +41 -0
- package/test-provider-run-lifecycle.js +349 -0
- package/test-provider-runner.js +271 -0
- package/test-provider-scheduler.js +689 -0
- package/test-provider-session-commands.js +185 -0
- package/test-provider-session-history.js +205 -0
- package/test-provider-side-chat.js +337 -0
- package/test-provider-state-migration.js +316 -0
- package/test-provider-stream-decoder.js +69 -0
- package/test-provider-subagent.js +228 -0
- package/test-provider-tool-hooks.js +360 -0
- package/test-recall-discoverer.js +1 -0
- package/test-recall-engine.js +3 -3
- package/test-recall-evolution.js +18 -0
- package/test-runner-watchdog-static.js +16 -8
- package/test-single-runtime.js +35 -0
- package/test-telegram-poll-recovery.js +56 -0
- package/test-tool-guard.js +56 -0
- package/test-tools.js +19 -0
- package/test-unified-identity.js +3 -1
- package/test-usage-accounting.js +92 -20
- package/test-utility-provider-policy.js +486 -0
- package/web.js +130 -35
package/core/web-auth.js
CHANGED
|
@@ -6,13 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
const { spawn } = require("child_process");
|
|
8
8
|
const os = require("os");
|
|
9
|
-
const {
|
|
9
|
+
const { discoverProviderExecutable } = require("./config");
|
|
10
|
+
const { inspectProviderConfiguration } = require("./provider-status");
|
|
10
11
|
const { redactSensitive, stripTerminalControls, extractUrls } = require("./redact");
|
|
11
12
|
const {
|
|
12
|
-
claudeSubprocessEnv, saveClaudeOAuthToken, getClaudeOAuthToken,
|
|
13
|
+
claudeSubprocessEnv, codexSubprocessEnv, saveClaudeOAuthToken, getClaudeOAuthToken,
|
|
13
14
|
extractClaudeToken, isClaudeAuthUrl, looksLikeClaudeToken, looksLikeOpenAIKey,
|
|
14
|
-
|
|
15
|
-
runCodexLoginStatus, saveCodexApiKeyWithCli,
|
|
15
|
+
saveCodexApiKeyWithCli,
|
|
16
16
|
} = require("./auth-flow");
|
|
17
17
|
|
|
18
18
|
const HOME = process.env.HOME || os.homedir();
|
|
@@ -22,7 +22,9 @@ let claude = null;
|
|
|
22
22
|
|
|
23
23
|
function startClaudeLogin() {
|
|
24
24
|
if (claude && claude.proc && !claude.done) return { ok: true, already: true };
|
|
25
|
-
const
|
|
25
|
+
const binary = discoverProviderExecutable("claude");
|
|
26
|
+
if (!binary) return { ok: false, error: "Claude Code CLI not found." };
|
|
27
|
+
const proc = spawn(binary, ["auth", "login", "--claudeai"], {
|
|
26
28
|
cwd: HOME, env: claudeSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
27
29
|
});
|
|
28
30
|
claude = { proc, url: null, log: "", awaitingCode: false, done: false, ok: false, error: null, tokenStored: false };
|
|
@@ -89,10 +91,11 @@ function saveClaudeToken(token) {
|
|
|
89
91
|
let codex = null;
|
|
90
92
|
|
|
91
93
|
function startCodexLogin() {
|
|
92
|
-
|
|
94
|
+
const binary = discoverProviderExecutable("codex");
|
|
95
|
+
if (!binary) return { ok: false, error: "Codex CLI not found." };
|
|
93
96
|
if (codex && codex.proc && !codex.done) return { ok: true, already: true };
|
|
94
|
-
const proc = spawn(
|
|
95
|
-
cwd: HOME, env:
|
|
97
|
+
const proc = spawn(binary, ["login", "--device-auth"], {
|
|
98
|
+
cwd: HOME, env: codexSubprocessEnv(), stdio: ["pipe", "pipe", "pipe"],
|
|
96
99
|
});
|
|
97
100
|
codex = { proc, url: null, code: null, log: "", done: false, ok: false, error: null };
|
|
98
101
|
const onChunk = (buf) => {
|
|
@@ -135,7 +138,7 @@ function codexState() {
|
|
|
135
138
|
|
|
136
139
|
// Reliable path: save a pasted OpenAI API key via the Codex CLI.
|
|
137
140
|
async function saveCodexApiKey(key) {
|
|
138
|
-
if (!
|
|
141
|
+
if (!discoverProviderExecutable("codex")) return { ok: false, error: "Codex CLI not found." };
|
|
139
142
|
const k = String(key || "").trim();
|
|
140
143
|
if (!looksLikeOpenAIKey(k)) return { ok: false, error: "That does not look like an OpenAI API key." };
|
|
141
144
|
const result = await saveCodexApiKeyWithCli(k);
|
|
@@ -145,11 +148,24 @@ async function saveCodexApiKey(key) {
|
|
|
145
148
|
// ── Combined status ─────────────────────────────────────────────────
|
|
146
149
|
function authStatus() {
|
|
147
150
|
const tokenInfo = getClaudeOAuthToken();
|
|
148
|
-
const
|
|
149
|
-
const
|
|
151
|
+
const summary = inspectProviderConfiguration();
|
|
152
|
+
const claudeStatus = summary.providers.find((provider) => provider.id === "claude");
|
|
153
|
+
const codexStatus = summary.providers.find((provider) => provider.id === "codex");
|
|
150
154
|
return {
|
|
151
|
-
|
|
152
|
-
|
|
155
|
+
providers: summary.providers,
|
|
156
|
+
defaultProvider: summary.defaultProvider,
|
|
157
|
+
// Transitional fields keep existing dashboard clients working while the
|
|
158
|
+
// generic provider list is the authoritative status surface.
|
|
159
|
+
claude: {
|
|
160
|
+
configured: !!tokenInfo.value,
|
|
161
|
+
source: tokenInfo.source,
|
|
162
|
+
cliFound: claudeStatus?.availability.state === "available",
|
|
163
|
+
loggedIn: claudeStatus?.auth.state === "authenticated",
|
|
164
|
+
},
|
|
165
|
+
codex: {
|
|
166
|
+
cliFound: codexStatus?.availability.state === "available",
|
|
167
|
+
loggedIn: codexStatus?.auth.state === "authenticated",
|
|
168
|
+
},
|
|
153
169
|
};
|
|
154
170
|
}
|
|
155
171
|
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
# Open Claudia — Channel design (Phase 0/2)
|
|
2
|
+
|
|
3
|
+
> Provider-parity update: current direct and agent modes both run through the
|
|
4
|
+
> modular `bot.js` entrypoint. `bot-agent.js` is now only a compatibility shim.
|
|
5
|
+
> Channel adapters dispatch normalized turns to the provider registry and do
|
|
6
|
+
> not choose a provider or construct provider-specific CLI arguments.
|
|
7
|
+
> Provider state/session/job changes use the verified snapshot and rollback
|
|
8
|
+
> process in [PROVIDER_MIGRATION.md](./PROVIDER_MIGRATION.md). References below
|
|
9
|
+
> to leaving the old monolith unchanged describe the historical channel refactor.
|
|
10
|
+
|
|
11
|
+
Companion to MULTI_CHANNEL_PLAN.md. Decides the concrete shape of channels in code before refactor begins.
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## 1. Channel-config mechanism
|
|
16
|
+
|
|
17
|
+
Channels are a configurable list. Each entry declares an adapter type + adapter-specific env keys. No hardcoded if/else in core.
|
|
18
|
+
|
|
19
|
+
`.env` example (both enabled):
|
|
20
|
+
```
|
|
21
|
+
CHANNELS=telegram,kazee
|
|
22
|
+
|
|
23
|
+
# telegram adapter
|
|
24
|
+
TELEGRAM_BOT_TOKEN=...
|
|
25
|
+
TELEGRAM_CHAT_ID=6251055967
|
|
26
|
+
|
|
27
|
+
# kazee adapter
|
|
28
|
+
KAZEE_URL=https://chat.kazee.africa
|
|
29
|
+
KAZEE_BOT_TOKEN=bot_...
|
|
30
|
+
KAZEE_BOT_USER_ID=64f8...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Backwards compat: if `CHANNELS` is unset, default to `telegram` so existing installs keep working (no migration on v1.21.0).
|
|
34
|
+
|
|
35
|
+
`core/config.js` exports `loadChannels()` returning `[{id, type, opts}]`. Bot bootstrap iterates and calls `createAdapter(type, opts)` per entry. Unknown type → log + skip, not fatal.
|
|
36
|
+
|
|
37
|
+
## 2. ChannelAdapter contract
|
|
38
|
+
|
|
39
|
+
One JS class per channel. All identified IO with the outside world (Telegram API, Kazee REST/socket) lives behind this surface — core code never touches transport SDKs directly.
|
|
40
|
+
|
|
41
|
+
```js
|
|
42
|
+
// channels/types.js — JSDoc-only contract (no runtime base class)
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* @typedef {Object} InboundEnvelope
|
|
46
|
+
* @property {string} channelId // raw transport id (telegram chatId, kazee chatId)
|
|
47
|
+
* @property {string} canonicalUserId // "telegram:123" | "kazee:abc"
|
|
48
|
+
* @property {string} userId // raw transport user id
|
|
49
|
+
* @property {string} [text]
|
|
50
|
+
* @property {string} [replyToId]
|
|
51
|
+
* @property {Array<MediaRef>} [mediaRefs]
|
|
52
|
+
* @property {string} type // "text" | "voice" | "audio" | "photo" | "document" | "action"
|
|
53
|
+
* @property {any} raw // original payload, adapter-specific
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @typedef {Object} SendOpts
|
|
58
|
+
* @property {string} [parseMode]
|
|
59
|
+
* @property {Object} [keyboard] // {inline_keyboard:[[{text,callback_data}]]} OR
|
|
60
|
+
* // {buttons:[{id,label,style,payload}]} for kazee interactive
|
|
61
|
+
* @property {string|number} [replyTo]
|
|
62
|
+
*/
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Every adapter implements:
|
|
66
|
+
* id: string "telegram" | "kazee"
|
|
67
|
+
* start(): Promise<void>
|
|
68
|
+
* stop(): Promise<void>
|
|
69
|
+
* on(event, fn): unsubscribe events: "message" | "action"
|
|
70
|
+
* send(channelId, text, opts): Promise<messageId|null>
|
|
71
|
+
* edit(channelId, messageId, text, opts): Promise<void>
|
|
72
|
+
* delete(channelId, messageId): Promise<void>
|
|
73
|
+
* sendVoice(channelId, oggPath): Promise<boolean>
|
|
74
|
+
* sendFile(channelId, filePath, caption?): Promise<boolean>
|
|
75
|
+
* typing(channelId): Promise<void>
|
|
76
|
+
* downloadMedia(mediaRef): Promise<localPath>
|
|
77
|
+
*/
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Notes
|
|
81
|
+
- `keyboard` is the one leaky abstraction. Telegram's `inline_keyboard` and Kazee's `interactive.buttons` differ enough that core builds a portable representation and each adapter renders it natively. Core uses `{buttons: [{id, label, style?, payload?}]}` as the canonical shape; the telegram adapter converts to `inline_keyboard`.
|
|
82
|
+
- `channelId` is opaque to core. Adapters route on it. Same chatId may exist on different channels — disambiguation happens via the adapter the message arrived on.
|
|
83
|
+
- `canonicalUserId` is the only user identifier core code cares about. It maps to per-user state.
|
|
84
|
+
|
|
85
|
+
## 3. Channel-aware chat context
|
|
86
|
+
|
|
87
|
+
Today: `chatContext = new AsyncLocalStorage()` stores chatId as a plain string. Every `send/edit/...` reads `currentChatId()`.
|
|
88
|
+
|
|
89
|
+
After: context stores `{adapter, channelId, canonicalUserId}`. New helpers:
|
|
90
|
+
```js
|
|
91
|
+
core/context.js
|
|
92
|
+
runInChat({adapter, channelId, canonicalUserId}, fn)
|
|
93
|
+
currentAdapter() // returns the ChannelAdapter for the in-flight request
|
|
94
|
+
currentChannelId() // raw channel id
|
|
95
|
+
currentCanonicalUserId()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
`send(text, opts)` becomes `currentAdapter().send(currentChannelId(), text, opts)`. All 19 Telegram primitives flatten down to ~7 adapter methods + utility helpers.
|
|
99
|
+
|
|
100
|
+
## 4. Command registry
|
|
101
|
+
|
|
102
|
+
`core/commands.js` exports a registry:
|
|
103
|
+
```js
|
|
104
|
+
register({
|
|
105
|
+
name: "model",
|
|
106
|
+
description: "Pick or show active model",
|
|
107
|
+
args: [{name: "model", required: false}],
|
|
108
|
+
handler: async (ctx, args) => { ... },
|
|
109
|
+
});
|
|
110
|
+
```
|
|
111
|
+
- `ctx` = `{canonicalUserId, channelId, adapter, raw, replyToId}`.
|
|
112
|
+
- `args` = `string[]` (raw tokens after `/cmd`), parser is intentionally dumb so handlers keep flexibility.
|
|
113
|
+
- The router parses `/cmd args` from inbound text and dispatches. Both adapters share one parser.
|
|
114
|
+
- Telegram-specific behaviour (regex multimatch like `/cron add "..." "..." "..."`) — handlers that need richer parsing can fall back to raw text via `ctx.raw.text`.
|
|
115
|
+
|
|
116
|
+
On startup the bot:
|
|
117
|
+
1. Loads registry.
|
|
118
|
+
2. Telegram adapter: no-op (Telegram has its own BotFather menu we don't auto-register; current behaviour preserved).
|
|
119
|
+
3. Kazee adapter: `PUT /bot/<botId>/commands` with `[{name, description, args}]` so web/mobile slash menus populate.
|
|
120
|
+
|
|
121
|
+
## 5. Refactor strategy (low-risk, incremental)
|
|
122
|
+
|
|
123
|
+
The 3544-line bot.js gets split across multiple commits, each shippable on its own and behaviour-preserving for Telegram.
|
|
124
|
+
|
|
125
|
+
| Step | Commit | Scope | Risk |
|
|
126
|
+
|---|---|---|---|
|
|
127
|
+
| A | extract config | `core/config.js` (loadEnv + loadChannels) | nil |
|
|
128
|
+
| B | extract identity | `core/identity.js` (canonicalForChannel, identities file IO) | nil |
|
|
129
|
+
| C | extract state | `core/state.js` (userStates, savedState, sessions persistence) | low |
|
|
130
|
+
| D | extract commands registry | `core/commands.js` empty registry + router that delegates to existing `bot.onText` handlers | nil (no behaviour change) |
|
|
131
|
+
| E | introduce ChannelAdapter | `channels/telegram/adapter.js` wraps existing `bot.sendMessage/...`. `chatContext` now stores `{adapter,channelId}`. All `send/edit/delete/sendVoice` in bot.js refactored to delegate to `currentAdapter()` | medium — touches every IO call site |
|
|
132
|
+
| F | migrate handlers to registry | Convert each `bot.onText` into a `register({...})` entry. Telegram adapter routes inbound text → registry. | medium — 35+ touchpoints, but mechanical |
|
|
133
|
+
| G | ship v1.21.0 | smoke test Telegram parity end-to-end | — |
|
|
134
|
+
| H | kazee adapter | `channels/kazee/adapter.js` — socket client, send/edit/delete/sendFile/sendVoice, interactive button shape | new code, no Telegram risk |
|
|
135
|
+
| I | ship v1.22.0 | kazee adapter behind `CHANNELS=telegram,kazee` once dev verified | — |
|
|
136
|
+
|
|
137
|
+
After step G the file tree looks like:
|
|
138
|
+
```
|
|
139
|
+
open-claudia/
|
|
140
|
+
bot.js # ~200 lines — bootstrap, signal handling, channel loop
|
|
141
|
+
core/
|
|
142
|
+
config.js
|
|
143
|
+
identity.js
|
|
144
|
+
state.js
|
|
145
|
+
context.js
|
|
146
|
+
commands.js
|
|
147
|
+
router.js # parses /cmd, dispatches registry, handles voice/media
|
|
148
|
+
transcripts.js # re-export of project-transcripts.js
|
|
149
|
+
channels/
|
|
150
|
+
types.js # JSDoc
|
|
151
|
+
telegram/
|
|
152
|
+
adapter.js
|
|
153
|
+
format.js # Telegram-MD quirks (single-asterisk bold)
|
|
154
|
+
bot-agent.js # unchanged
|
|
155
|
+
health.js # unchanged
|
|
156
|
+
web.js # unchanged
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
After step H:
|
|
160
|
+
```
|
|
161
|
+
channels/kazee/
|
|
162
|
+
adapter.js
|
|
163
|
+
format.js # standard markdown, no chunking
|
|
164
|
+
socket.js # socket.io-client wrapper
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## 6. Open items I'm flagging now
|
|
168
|
+
|
|
169
|
+
1. **Telegram `parseMode: "Markdown"` vs `MarkdownV2`** — many existing `send(text, {parseMode: "Markdown"})` calls. Kazee adapter ignores parseMode (always standard MD). Need adapter-side normalisation if we ever ship the same text through both. For Phase 0, parseMode flows through unchanged for Telegram; Kazee adapter silently drops it.
|
|
170
|
+
2. **Reply IDs across channels** — `replyTo` is a Telegram `message_id`. Kazee uses Mongo `_id`. Already opaque in `SendOpts`. Fine.
|
|
171
|
+
3. **`bot.on("callback_query")`** in Telegram becomes a `{type:"action", actionId, payload}` envelope on the adapter. Same path Kazee uses for button presses. Core router sees both the same way.
|
|
172
|
+
4. **Voice download path** — `downloadFile(fileId, ext)` is Telegram-specific. Becomes `adapter.downloadMedia(mediaRef)` returning a local file path. Telegram adapter wraps `bot.getFile + https.get`; Kazee adapter does `GET <minio-url>`.
|
|
173
|
+
5. **Crons & notifyError** — both currently call `bot.sendMessage(CHAT_ID, ...)` directly. Need to route through the *owner's* preferred adapter. Cron entries store `canonicalUserId`; lookup their preferred channel via identities and dispatch via that adapter. (`notifyError` keeps Telegram-only fallback — last-resort crash channel.)
|
|
174
|
+
|
|
175
|
+
## 7. What I won't change in this refactor
|
|
176
|
+
|
|
177
|
+
- At that time, `bot-agent.js` stayed as the Claude runner subprocess wrapper; it is now a provider-neutral compatibility shim.
|
|
178
|
+
- `health.js`, `web.js`, `setup.js` — untouched.
|
|
179
|
+
- Historical subprocess management stayed unchanged during this channel-only refactor; provider convergence happened later.
|
|
180
|
+
- Vault, soul, crons schemas on disk — same files, same format. No migration.
|
|
181
|
+
- Identities file format — adds `kazee:<userId>` keys naturally; no schema change needed.
|
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
# Open Claudia — Multi-Channel Plan (Telegram + Kazee Chat)
|
|
2
|
+
|
|
3
|
+
> This is a historical design plan. The target adapter architecture has since
|
|
4
|
+
> shipped; Section 1 records the current Open Claudia implementation, while the
|
|
5
|
+
> later rollout estimates and external-client notes are retained as design history.
|
|
6
|
+
|
|
7
|
+
## Goal
|
|
8
|
+
Make Open Claudia channel-agnostic, with Telegram as one adapter and **Kazee Chat** as a second equal-class adapter. Slash commands must work natively in both.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## 1. Current implementation
|
|
13
|
+
|
|
14
|
+
### open-claudia
|
|
15
|
+
- `bot.js` is the modular runtime bootstrap; Telegram, Kazee, and voice are channel adapters behind one normalized router and command registry.
|
|
16
|
+
- Async chat context carries the adapter, channel ID, and canonical user ID. Linked channels can resolve to one canonical identity and share provider/project state safely.
|
|
17
|
+
- All model-bearing paths call the provider-neutral runner. The provider registry owns Claude Code and OpenAI Codex capabilities, invocation arguments, event parsing, auth status, and native session semantics.
|
|
18
|
+
- Setup, web configuration, doctor, and status remain available with no provider installed. Model turns require one compatible authenticated provider.
|
|
19
|
+
- Provider-aware state, sessions, and scheduled jobs use the verified snapshot and rollback procedure in [PROVIDER_MIGRATION.md](./PROVIDER_MIGRATION.md).
|
|
20
|
+
- Shared operator stores such as the vault and soul remain intentionally global; conversations, settings, usage, and transcripts are canonical-user/project/provider scoped.
|
|
21
|
+
|
|
22
|
+
### Historical external-service baseline: chat-central (ActionHero + Socket.IO + Mongo + Redis)
|
|
23
|
+
- **Bot infrastructure already exists**:
|
|
24
|
+
- `User.type: "Bot"` with `botToken` field (unique, indexed).
|
|
25
|
+
- Auth middleware accepts `Authorization: Bearer bot_<token>` and bypasses Central JWT.
|
|
26
|
+
- `BotRole` model with `webhookUrl` (for external webhook fan-out).
|
|
27
|
+
- `createBot`, `listBots`, `createBotRole`, `listBotRoles` actions exist.
|
|
28
|
+
- Message model is rich: text/image/video/file/audio/poll/system, replyTo, mentions, reactions, edits, media[], read receipts.
|
|
29
|
+
- Real-time via Socket.IO V2 — events: `message:new`, `message:update`, `message:delete`, `typing`, `reaction:updated`, `pin:updated`, `chat:read`, etc.
|
|
30
|
+
- DMs: `isGroupChat: false`, 2 participants. Existing dedup query.
|
|
31
|
+
- File upload: `POST /chat/:chat_id/media` (MinIO-backed).
|
|
32
|
+
- **No native slash-command framework** — content is plain text. We need to build the slash menu UX in the clients.
|
|
33
|
+
|
|
34
|
+
### kazee-chat (web, Next.js + TipTap + Socket.IO V2)
|
|
35
|
+
- Rich text editor (TipTap) for input. Mentions autocomplete exists; **no slash menu yet**.
|
|
36
|
+
- Markdown rendering (react-markdown + rehype-highlight) — good for our outputs.
|
|
37
|
+
- File upload, voice recording, image rendering — all working.
|
|
38
|
+
|
|
39
|
+
### kazee-chat-mobile (React Native + Expo + Socket.IO V2)
|
|
40
|
+
- @-mention autocomplete in `ChatInputToolbar.tsx`; **no slash menu yet**.
|
|
41
|
+
- Voice recording via `expo-audio`; uploads via `mediaPipeline`.
|
|
42
|
+
- Markdown rendering via `react-native-markdown-display`.
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 2. Target architecture
|
|
47
|
+
|
|
48
|
+
### 2.1 ChannelAdapter interface (open-claudia)
|
|
49
|
+
A single interface every transport implements. The bot core becomes transport-agnostic and only talks to adapters.
|
|
50
|
+
|
|
51
|
+
```js
|
|
52
|
+
// channels/types.d.ts (conceptual)
|
|
53
|
+
class ChannelAdapter {
|
|
54
|
+
readonly id; // "telegram" | "kazee"
|
|
55
|
+
|
|
56
|
+
// Lifecycle
|
|
57
|
+
start(); // connect/poll
|
|
58
|
+
stop();
|
|
59
|
+
|
|
60
|
+
// Inbound (emit to core)
|
|
61
|
+
on("message", fn(InboundMessage));
|
|
62
|
+
on("command", fn(InboundCommand)); // parsed slash command
|
|
63
|
+
on("media", fn(InboundMedia)); // voice/photo/document
|
|
64
|
+
on("action", fn(InboundAction)); // button click / callback
|
|
65
|
+
|
|
66
|
+
// Outbound (called by core)
|
|
67
|
+
send(channelId, text, opts); // {parseMode, replyTo, buttons}
|
|
68
|
+
edit(channelId, messageId, text, opts);
|
|
69
|
+
delete(channelId, messageId);
|
|
70
|
+
sendFile(channelId, path, caption);
|
|
71
|
+
sendVoice(channelId, path);
|
|
72
|
+
typing(channelId);
|
|
73
|
+
|
|
74
|
+
// Identity
|
|
75
|
+
canonicalUserId(rawId); // "telegram:123" / "kazee:abc"
|
|
76
|
+
downloadMedia(mediaRef) -> localPath;
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`InboundMessage` is a normalised envelope:
|
|
81
|
+
```ts
|
|
82
|
+
{ channelId, userId, canonicalUserId, text, replyToId, mediaRefs[], raw }
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 2.2 Bot core (transport-agnostic)
|
|
86
|
+
- `core/router.js` — receives normalised events, dispatches to command registry or chat handler.
|
|
87
|
+
- `core/commands.js` — explicit registry: `{name, description, args, handler}`. Both adapters consume the same registry; Telegram converts to `bot.onText` regex, Kazee exposes it via REST so clients can build the slash menu.
|
|
88
|
+
- `core/state.js` — already mostly transport-agnostic (sessions, vault, soul, crons). Stays.
|
|
89
|
+
- `core/identity.js` — `canonicalForChannel(transport, channelId)` already exists; generalise to `kazee:` prefix.
|
|
90
|
+
|
|
91
|
+
### 2.3 Refactor scope (bot.js → modules)
|
|
92
|
+
| New module | Lifted from bot.js (approx lines) | Notes |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| `channels/telegram/index.js` | client init 228-232, polling 238-250, all `bot.onText` 755-3091, media handlers 3259-3339, callback_query 3102-3255, `send/edit/delete/getFile` wrappers 986-1113 | Becomes a thin adapter |
|
|
95
|
+
| `channels/kazee/index.js` | NEW | Socket.IO V2 client + REST send + media download |
|
|
96
|
+
| `core/router.js` | NEW — dispatch logic from `bot.on("message")` 3343-3497 |
|
|
97
|
+
| `core/commands.js` | extract command handlers + descriptions | Each handler becomes `{name, desc, args, fn}` |
|
|
98
|
+
| `core/identity.js` | 376-392, 487-491, 561-589 | Generalised |
|
|
99
|
+
| `core/transcripts.js` | already split (project-transcripts.js) | No change |
|
|
100
|
+
| `core/state.js` | 501-503, 597-649 | No change |
|
|
101
|
+
|
|
102
|
+
bot.js shrinks to ~200 lines of bootstrap.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## 3. Kazee Chat adapter — concrete plan
|
|
107
|
+
|
|
108
|
+
### 3.1 Bot account setup (one-time, ops)
|
|
109
|
+
On chat-central:
|
|
110
|
+
1. `POST /botRole` → `{botName: "open-claudia", webhookUrl: null}` (we won't use webhook; we use Socket.IO).
|
|
111
|
+
2. `POST /bot` → creates `User{type:"Bot", botToken, botRole, name:"Open Claudia"}`.
|
|
112
|
+
3. Store `botToken` in open-claudia env (`KAZEE_BOT_TOKEN`) — never in code.
|
|
113
|
+
|
|
114
|
+
### 3.2 Connect (open-claudia → chat-central)
|
|
115
|
+
- Open Socket.IO V2 connection: `wss://<chat-central>/socket.io?version=v2` with `Authorization: Bearer bot_<token>`.
|
|
116
|
+
- On connect: `GET /chats?userId=<botUserId>` → list of chats the bot is in.
|
|
117
|
+
- Batch-join those chat rooms (same pattern web/mobile use).
|
|
118
|
+
- Listen to `message:new`. Filter: ignore own messages, process DMs + group messages mentioning bot.
|
|
119
|
+
|
|
120
|
+
### 3.3 Identity
|
|
121
|
+
- Canonical user ID: `kazee:<userId>` (chat-central User._id).
|
|
122
|
+
- Per-user transcripts: same isolation logic as Telegram — `projectHash` already userId-scoped.
|
|
123
|
+
|
|
124
|
+
### 3.4 Send / edit / delete
|
|
125
|
+
- Send: `POST /chat/:chatId/message` with `{content, type:"text"}` or `{type:"image", mediaIds}`.
|
|
126
|
+
- Edit: there's no documented edit endpoint in the explore report; we either (a) add `PATCH /message/:id` to chat-central, or (b) skip in-place edits and just send a follow-up. **Recommend (a)** — chat already supports `edited` + `editHistory` in the model.
|
|
127
|
+
- Delete: similar — likely needs a `DELETE /message/:id` action if not present. **Confirm in chat-central** before assuming.
|
|
128
|
+
|
|
129
|
+
### 3.5 Voice / files
|
|
130
|
+
- Receive: when `message:new` arrives with `type:"audio"`/`"voice"`, fetch media URL, download via REST, run Whisper (same as today's `transcribeAudio`).
|
|
131
|
+
- Send file: `POST /chat/:chatId/media` (returns mediaId) → `POST /chat/:chatId/message` with `mediaIds:[id]`.
|
|
132
|
+
|
|
133
|
+
### 3.6 Buttons / callback queries (the awkward gap)
|
|
134
|
+
Telegram inline keyboards have no Kazee equivalent today. Used in open-claudia for: auth approval, project picker, model/backend/effort selection, session switcher, onboarding style, cron presets.
|
|
135
|
+
|
|
136
|
+
Three options, ranked:
|
|
137
|
+
1. **Recommended — add a `type:"interactive"` message kind to chat-central** with a `buttons:[{label, callbackId}]` field, plus a `message:action` socket event when a user clicks. Render natively in web + mobile. Reuses existing message infra. Highest UX quality.
|
|
138
|
+
2. **Text-fallback** — bot sends `Reply with 1/2/3` style menus, parses replies. No frontend work, ugly UX, fragile when humans free-type.
|
|
139
|
+
3. **Web-only mini-form** — bot embeds a special markdown block (`:::buttons[...]:::`), web/mobile parse and render. Lighter than (1), heavier than (2). Drifts from chat-central message model.
|
|
140
|
+
|
|
141
|
+
Decision needed before implementation — flagging this as the biggest design choice.
|
|
142
|
+
|
|
143
|
+
### 3.7 Markdown
|
|
144
|
+
- Standard Markdown — react-markdown / react-native-markdown-display already render `**bold**`, `_italic_`, `` `code` ``, fenced code, lists. Bot's existing Telegram-Markdown output is mostly compatible; minor: Telegram uses `*bold*` (single asterisk). The adapter normalises asterisks on the way out.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 4. Slash commands — UX across clients
|
|
149
|
+
|
|
150
|
+
### 4.1 Server side (chat-central, new)
|
|
151
|
+
- Add `commands: [{name, description, args:[{name, required}]}]` to `User` (for Bot type) or to `BotRole`. Prefer `BotRole.commands`.
|
|
152
|
+
- New action: `GET /bot/:userId/commands` → returns the list.
|
|
153
|
+
- Bot registers/updates its command list on startup via `PUT /botRole/:id/commands`.
|
|
154
|
+
- Estimated effort: 1 model field, 2 actions, ~1 day in chat-central.
|
|
155
|
+
|
|
156
|
+
### 4.2 Web (kazee-chat) — slash menu in `ChatInput.tsx`
|
|
157
|
+
- TipTap extension or input-level handler that:
|
|
158
|
+
- Detects `/` as first character of an empty message.
|
|
159
|
+
- Calls `GET /bot/:botUserId/commands` if the other party is a Bot user.
|
|
160
|
+
- Shows a floating dropdown (same component family as mentions menu).
|
|
161
|
+
- On selection: inserts the slash command name, leaves cursor for args.
|
|
162
|
+
- Caches command list per bot for the session.
|
|
163
|
+
- Submit sends `content: "/cmd arg1 arg2"` as normal text — bot parses on its side.
|
|
164
|
+
- Estimated effort: ~2 days.
|
|
165
|
+
|
|
166
|
+
### 4.3 Mobile (kazee-chat-mobile) — slash menu in `ChatInputToolbar.tsx`
|
|
167
|
+
- Same logic, RN implementation. Reuse the mentions autocomplete pattern that already exists.
|
|
168
|
+
- Floating list above keyboard.
|
|
169
|
+
- Estimated effort: ~2 days.
|
|
170
|
+
|
|
171
|
+
### 4.4 Bot side (open-claudia)
|
|
172
|
+
- Parse `/cmd args` from inbound text. `core/commands.js` registry already handles this — no Telegram dependency.
|
|
173
|
+
- Same handlers work on both adapters.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 5. Phased rollout
|
|
178
|
+
|
|
179
|
+
**Phase 0 — prep (no behaviour change)**
|
|
180
|
+
- Refactor `bot.js` to extract `core/commands.js`, `core/router.js`, `channels/telegram/`. All existing functionality goes through the new abstraction. Ship as v1.21.0. Verify Telegram still works.
|
|
181
|
+
|
|
182
|
+
**Phase 1 — chat-central additions**
|
|
183
|
+
- Add `BotRole.commands` field + `GET /bot/:id/commands`.
|
|
184
|
+
- Add `PATCH /message/:id` (edit) + `DELETE /message/:id` (delete) actions, if not present.
|
|
185
|
+
- Decision on interactive buttons (§3.6). If option 1 (new message type), spec + implement.
|
|
186
|
+
|
|
187
|
+
**Phase 2 — kazee adapter**
|
|
188
|
+
- Build `channels/kazee/index.js` in open-claudia.
|
|
189
|
+
- Provision bot account in chat-central (dev env first).
|
|
190
|
+
- Connect, listen, send text. Parity with Telegram for plain text + file + voice.
|
|
191
|
+
- Ship as v1.22.0 behind `KAZEE_ENABLED=true` env flag.
|
|
192
|
+
|
|
193
|
+
**Phase 3 — slash menu UX**
|
|
194
|
+
- Web: slash menu in `ChatInput.tsx`.
|
|
195
|
+
- Mobile: slash menu in `ChatInputToolbar.tsx`.
|
|
196
|
+
- Bot registers its full command list (currently 35+).
|
|
197
|
+
|
|
198
|
+
**Phase 4 — interactive buttons (if option 1 chosen)**
|
|
199
|
+
- chat-central: `type:"interactive"` message + `message:action` event.
|
|
200
|
+
- Web + mobile: render buttons in `MessageBubble`, emit action on tap.
|
|
201
|
+
- Bot: emit interactive menus instead of text for auth/project/model selection.
|
|
202
|
+
|
|
203
|
+
**Phase 5 — production**
|
|
204
|
+
- Flip `KAZEE_ENABLED` on in prod.
|
|
205
|
+
- Document onboarding: how a Kazee user starts a chat with Open Claudia and authenticates.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
## 6. Onboarding flow (Kazee)
|
|
210
|
+
|
|
211
|
+
1. User opens Kazee, searches for "Open Claudia" in users.
|
|
212
|
+
2. Creates DM (existing flow: `createChat` with bot as participant).
|
|
213
|
+
3. Bot receives `message:new` for the first message → triggers `/start`-equivalent: replies with auth instructions.
|
|
214
|
+
4. Auth: same model as Telegram today (vault-stored credentials, OAuth flow). No changes — `canonicalUserId = kazee:<userId>` ensures isolation.
|
|
215
|
+
5. Subsequent messages flow normally.
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
## 7. Risks & open questions
|
|
220
|
+
|
|
221
|
+
| # | Risk | Mitigation |
|
|
222
|
+
|---|---|---|
|
|
223
|
+
| 1 | Edit/delete endpoints may not exist in chat-central today | Verify before Phase 1; add if missing |
|
|
224
|
+
| 2 | Telegram inline keyboards have no equivalent — biggest design call | Decide §3.6 option before Phase 4 |
|
|
225
|
+
| 3 | Markdown dialect drift (Telegram `*bold*` vs standard `**bold**`) | Normalise in `channels/telegram/format.js` and `channels/kazee/format.js` |
|
|
226
|
+
| 4 | Long-message chunking (4KB Telegram limit) doesn't apply to Kazee | Move chunking into Telegram adapter only |
|
|
227
|
+
| 5 | Voice transcription path coupled to TEMP_DIR + ffmpeg + whisper | Already path-based; just changes how media is fetched (REST vs Telegram API) |
|
|
228
|
+
| 6 | Bot in group chats: noise if it processes every message | Only respond when @mentioned or in DM (chat-central `mentions[]` already supports this) |
|
|
229
|
+
| 7 | Onboarding identity binding — how do we know which Kazee user is which workspace operator? | `/auth` flow stays the same; first message after `/auth` binds canonical user to bot state |
|
|
230
|
+
|
|
231
|
+
---
|
|
232
|
+
|
|
233
|
+
## 8. Work breakdown (rough)
|
|
234
|
+
|
|
235
|
+
| Area | Effort | Owner |
|
|
236
|
+
|---|---|---|
|
|
237
|
+
| Phase 0 — bot.js refactor | 3-4 days | open-claudia |
|
|
238
|
+
| Phase 1 — chat-central additions (commands list + edit/delete) | 2 days | chat-central |
|
|
239
|
+
| Phase 2 — Kazee adapter (open-claudia) | 3 days | open-claudia |
|
|
240
|
+
| Phase 3 — slash menu (web) | 2 days | kazee-chat |
|
|
241
|
+
| Phase 3 — slash menu (mobile) | 2 days | kazee-chat-mobile |
|
|
242
|
+
| Phase 4 — interactive buttons (optional but recommended) | 4 days across 3 repos | shared |
|
|
243
|
+
| **Total core (Phases 0–3)** | **~12 days** | |
|
|
244
|
+
| **With Phase 4** | **~16 days** | |
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## 9. Decisions to lock before starting
|
|
249
|
+
|
|
250
|
+
1. **Interactive buttons** — option 1, 2, or 3 from §3.6? Affects all three frontends and chat-central scope.
|
|
251
|
+
2. **Bot scope in group chats** — DMs only initially, or mention-driven in groups too?
|
|
252
|
+
3. **Command registration** — does bot register commands on every startup, or one-shot via ops script?
|
|
253
|
+
4. **Edit/delete endpoints in chat-central** — confirm they exist; if not, add to Phase 1.
|
|
254
|
+
5. **`KAZEE_ENABLED` flag** — env-flag rollout vs straight cutover.
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# Provider migration snapshots and rollback
|
|
2
|
+
|
|
3
|
+
Open Claudia takes a verified, byte-for-byte snapshot before any provider-aware state, session, or job schema is activated. At startup it then migrates and verifies all three stores before loading the vault, scheduler, router, or channel adapters. Direct and agent modes use the same modular `bot.js` runtime, so there is only one active state writer during this process.
|
|
4
|
+
|
|
5
|
+
Snapshots live under the configured Open Claudia data directory:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
migration-backups/provider-parity-v1.1/snapshots/<timestamp-id>/
|
|
9
|
+
originals/
|
|
10
|
+
manifest.json
|
|
11
|
+
manifest.sha256
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
The allowlist contains `state.json`, `sessions.json`, `jobs.json`, legacy `crons.json`, an earlier `crons.json.migrated`, and relevant `.bak` siblings. It deliberately excludes `.env`, vault, authentication, identity, transcript, and derived SQLite files. The manifest records relative names, byte sizes, SHA-256 hashes, and advisory schema guesses; it never records source contents or absolute installation paths. Malformed and provider-ambiguous records are preserved as `unresolved` rather than assigned to Claude, Codex, or a project by guesswork. Removed-provider selections, settings, session pointers/history, and jobs are retained only in non-selectable or disabled archives; they are never reassigned to Claude or Codex and never fired.
|
|
15
|
+
|
|
16
|
+
Snapshot directories are private operator data. They can contain conversation titles, prompts, native session identifiers, and scheduled-job text. Keep them mode `0700` with files mode `0600`; do not share, publish, or commit them.
|
|
17
|
+
|
|
18
|
+
## Rollback procedure
|
|
19
|
+
|
|
20
|
+
Rollback is an offline operation. Do not restore files while any Open Claudia process can write them.
|
|
21
|
+
|
|
22
|
+
1. Stop every Open Claudia bot and scheduler process. Confirm no old or new runtime remains active.
|
|
23
|
+
2. Identify the snapshot referenced by `migration-backups/provider-parity-v1.1/journal.json`. Do not select a partial orphan directory merely because its timestamp is newer.
|
|
24
|
+
3. Validate the complete snapshot with `validateMigrationSnapshot(snapshotDir)`. A manifest mismatch, missing file, corrupt copy, symlink, or checksum failure blocks rollback before any live target changes.
|
|
25
|
+
4. Call `rollbackMigrationSnapshot` from the installed Open Claudia package with the configured state/session/job/cron paths, as in the example below. The helper stages exact bytes, restores the complete allowlisted set, restores original absence where applicable, and verifies every target against the manifest.
|
|
26
|
+
5. Validate the snapshot again and verify the restored live files with the same source list. Keep the snapshot; rollback never deletes it.
|
|
27
|
+
6. If the restored files contain Cursor Agent state, restart the matching older Cursor-capable Open Claudia version. The current runtime intentionally cannot execute that provider. Otherwise restart the upgraded version only after its migration journal and schema expectations have been reviewed.
|
|
28
|
+
|
|
29
|
+
Rollback restores the snapshot's exact pre-upgrade bytes. Post-upgrade changes are not merged into those older files and will be discarded by the restore. If those changes must be audited, make a separate private copy of the current files while every writer is stopped; never combine records from the two schema generations by hand.
|
|
30
|
+
|
|
31
|
+
Example from a Node maintenance script run inside the installed package:
|
|
32
|
+
|
|
33
|
+
```js
|
|
34
|
+
const migration = require("./core/migration-backup");
|
|
35
|
+
const config = require("./core/config");
|
|
36
|
+
const configDir = config.CONFIG_DIR;
|
|
37
|
+
const snapshotDir = process.env.OPEN_CLAUDIA_MIGRATION_SNAPSHOT;
|
|
38
|
+
const sources = migration.defaultMigrationSources({
|
|
39
|
+
configDir,
|
|
40
|
+
stateFile: config.STATE_FILE,
|
|
41
|
+
sessionsFile: config.SESSIONS_FILE,
|
|
42
|
+
jobsFile: config.JOBS_FILE,
|
|
43
|
+
cronsFile: config.CRONS_FILE,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
migration.validateMigrationSnapshot(snapshotDir);
|
|
47
|
+
migration.rollbackMigrationSnapshot(snapshotDir, { configDir, sources });
|
|
48
|
+
migration.validateMigrationSnapshot(snapshotDir, { sources, verifySources: true });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Never mark a migration globally activated after migrating only one store. The journal tracks `state`, `sessions`, and `jobs` separately and reaches `activated` only after all three component migrations have completed and verified their writes. Each component certificate records the live file's logical path, byte size, schema version, and SHA-256; readiness recomputes those values, so a legacy, corrupt, or subsequently replaced file cannot satisfy an old activation flag.
|
|
52
|
+
|
|
53
|
+
Startup beyond the migration preflight is gated on that globally activated journal. A missing component, corrupt manifest, stale live-file proof, or partial migration blocks normal runtime initialization. After the gate, an old Cursor selection becomes an explicit “provider selection required” state; the user must choose Claude Code or OpenAI Codex with `/backend`. Old callbacks show a removal notice and do not mutate the provider, model, or session.
|
|
54
|
+
|
|
55
|
+
Each component write is two-phase. Open Claudia records a `pendingComponents` entry against the verified pre-migration snapshot before the atomic live-file rename, validates the new schema after the rename, and only then marks that component activated. If the process stops between those steps, startup keeps the original snapshot pinned; the component migration must validate and finish the pending write instead of creating a mixed-schema replacement snapshot.
|
|
56
|
+
|
|
57
|
+
Provider-blind conversation-history records are retained as non-selectable `legacy` entries unless exactly one provider adapter can prove ownership with a read-only probe. New history is keyed by canonical user, project, and provider. Chat buttons contain only short-lived opaque server-side tokens; provider-native session IDs are never embedded in callback payloads.
|
|
58
|
+
|
|
59
|
+
Provider and project switches restore only that tuple's active conversation, while model, effort, budget, permission mode, and worktree choices remain scoped to the provider. `/new` clears the selected project's active pointer for the current provider only. `/end` closes the project selection and settles transient queued work without deleting provider pointers or conversation history.
|
|
60
|
+
|
|
61
|
+
Compaction captures one immutable canonical-user/project/provider/session tuple. Summary and seed runs cannot persist ordinary transcripts, usage, or session pointers, and queued user turns remain paused until both stages settle. Claude seeds use its native one-turn limit in read-only plan mode. Codex seeds use a read-only sandbox plus a bounded JSON acknowledgement contract; they deliberately use a persistent compaction invocation instead of Codex's ephemeral utility flag because the resulting seed must remain resumable. No unsupported Codex `maxTurns` or complete tool-suppression claim is made.
|
|
62
|
+
|
|
63
|
+
The pointer, provider-tagged history lineage, and tuple-scoped usage checkpoint are committed through a recoverable prepared/committed marker. Restart rolls a prepared operation back to the old pointer and history, while a committed marker is finalized without replaying history. Legacy usage-log records expose their existing `backend` as `provider` when read, without rewriting the original JSONL file.
|
|
64
|
+
|
|
65
|
+
Scheduled work uses a versioned `{schemaVersion, jobs, archived}` document. Every runnable occurrence stores its canonical user, channel, project, provider, provider-settings snapshot, native session lineage, origin run, and stable occurrence ID. A fresh foreground turn can create a job before its native session is known; terminal session persistence binds that job by origin run ID. Compaction descendants are followed only when recorded lineage proves the relationship, and an older scheduled branch cannot replace an unrelated live session pointer.
|
|
66
|
+
|
|
67
|
+
Wakeups and crons wait for terminal provider completion, transcript/usage/session persistence, and required delivery. A one-shot receives a durable success marker before deletion. Busy or failed occurrences retain their occurrence ID and bounded attempt state across restart; exhaustion and wakeups outside the missed-fire grace window remain visible as disabled dead letters. Provider fallback is empty by default. When `PROVIDER_FALLBACKS` is explicitly configured, a fallback starts a fresh provider session with an archived provider-neutral brief and never receives another provider's native session ID. Legacy `crons.json` remains byte-for-byte untouched after its ambiguous records are copied into the disabled archive.
|