@inetafrica/open-claudia 2.14.8 → 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 +21 -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 +105 -7
- 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 +127 -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 +55 -0
- package/core/handlers.js +503 -217
- package/core/ideas.js +2 -1
- package/core/identity.js +8 -11
- package/core/io.js +24 -3
- package/core/jobs.js +589 -72
- package/core/lessons.js +3 -2
- package/core/loopback.js +45 -8
- 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 +1415 -1282
- package/core/scheduler.js +515 -210
- package/core/side-chat.js +346 -0
- package/core/state.js +1096 -95
- 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/core/web-sessions.js +78 -0
- 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 +51 -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-delivery-contract.js +88 -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-run-lock.js +63 -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/test-web-sessions.js +74 -0
- package/web.js +159 -45
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
const { spawnSync } = require("child_process");
|
|
10
|
+
|
|
11
|
+
const root = __dirname;
|
|
12
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-session-commands-"));
|
|
13
|
+
const workspace = path.join(configDir, "workspace");
|
|
14
|
+
fs.mkdirSync(path.join(workspace, "alpha"), { recursive: true });
|
|
15
|
+
fs.mkdirSync(path.join(workspace, "beta"), { recursive: true });
|
|
16
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = configDir;
|
|
17
|
+
process.env.WORKSPACE = workspace;
|
|
18
|
+
process.env.TELEGRAM_CHAT_ID = "fixture";
|
|
19
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
20
|
+
process.env.CLAUDE_PATH = process.execPath;
|
|
21
|
+
process.env.CODEX_PATH = process.execPath;
|
|
22
|
+
|
|
23
|
+
const stateApi = require("./core/state");
|
|
24
|
+
const handlers = require("./core/handlers");
|
|
25
|
+
const commands = require("./core/commands");
|
|
26
|
+
const { runInChat } = require("./core/context");
|
|
27
|
+
|
|
28
|
+
function seedMatrix(state) {
|
|
29
|
+
const user = state.userId;
|
|
30
|
+
for (const [project, provider, session, model, effort] of [
|
|
31
|
+
["alpha", "claude", "claude-alpha", "claude-alpha-model", "high"],
|
|
32
|
+
["alpha", "codex", "codex-alpha", "codex-alpha-model", "low"],
|
|
33
|
+
["beta", "claude", "claude-beta", "claude-beta-model", "medium"],
|
|
34
|
+
["beta", "codex", "codex-beta", "codex-beta-model", "xhigh"],
|
|
35
|
+
]) {
|
|
36
|
+
stateApi.recordSession(user, project, provider, session, `${provider} ${project}`, { strict: true });
|
|
37
|
+
stateApi.setProviderSession(state, provider, project, session);
|
|
38
|
+
const settings = stateApi.getProviderSettings(state, provider);
|
|
39
|
+
// Provider settings are provider-scoped rather than project-scoped. The
|
|
40
|
+
// final assignment for each provider is what switching must restore.
|
|
41
|
+
settings.model = model;
|
|
42
|
+
settings.effort = effort;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function switchingAndProjectProbe() {
|
|
47
|
+
const state = stateApi.currentState();
|
|
48
|
+
state.currentSession = { name: "alpha", dir: path.join(workspace, "alpha") };
|
|
49
|
+
state.settings.backend = "claude";
|
|
50
|
+
seedMatrix(state);
|
|
51
|
+
stateApi.getProviderSettings(state, "claude").model = "claude-restored";
|
|
52
|
+
stateApi.getProviderSettings(state, "claude").effort = "high";
|
|
53
|
+
stateApi.getProviderSettings(state, "codex").model = "codex-restored";
|
|
54
|
+
stateApi.getProviderSettings(state, "codex").effort = "low";
|
|
55
|
+
|
|
56
|
+
let switched = handlers.switchProviderState("codex", { state, persist: false });
|
|
57
|
+
assert.strictEqual(switched.ok, true);
|
|
58
|
+
assert.strictEqual(state.settings.backend, "codex");
|
|
59
|
+
assert.strictEqual(state.settings.model, "codex-restored");
|
|
60
|
+
assert.strictEqual(state.settings.effort, "low");
|
|
61
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), "codex-alpha");
|
|
62
|
+
|
|
63
|
+
switched = handlers.switchProviderState("claude", { state, persist: false });
|
|
64
|
+
assert.strictEqual(switched.ok, true);
|
|
65
|
+
assert.strictEqual(state.settings.model, "claude-restored");
|
|
66
|
+
assert.strictEqual(state.settings.effort, "high");
|
|
67
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), "codex-alpha", "Codex pointer survives Claude switch");
|
|
68
|
+
|
|
69
|
+
state.settings.backend = "codex";
|
|
70
|
+
handlers.selectProjectState({ name: "beta", dir: path.join(workspace, "beta") }, { state, persist: false });
|
|
71
|
+
assert.strictEqual(state.currentSession.name, "beta");
|
|
72
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "beta"), "codex-beta");
|
|
73
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "beta"), "claude-beta");
|
|
74
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), "codex-alpha");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function newEndAndContinueProbe() {
|
|
78
|
+
const state = stateApi.currentState();
|
|
79
|
+
state.currentSession = { name: "alpha", dir: path.join(workspace, "alpha") };
|
|
80
|
+
state.settings.backend = "codex";
|
|
81
|
+
const settled = [];
|
|
82
|
+
state.messageQueue = [{ deferred: { resolve: (value) => settled.push(value) } }];
|
|
83
|
+
|
|
84
|
+
const fresh = handlers.startNewConversation({ state, persist: false });
|
|
85
|
+
assert.strictEqual(fresh.ok, true);
|
|
86
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), null);
|
|
87
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "claude-alpha");
|
|
88
|
+
assert.strictEqual(stateApi.userOwnsProviderSession(state.userId, "alpha", "codex", "codex-alpha"), true, "new does not delete history");
|
|
89
|
+
assert.strictEqual(state.messageQueue.length, 0);
|
|
90
|
+
assert.strictEqual(settled.length, 1);
|
|
91
|
+
assert.strictEqual(settled[0].status, "cancelled");
|
|
92
|
+
|
|
93
|
+
stateApi.setProviderSession(state, "codex", "alpha", "codex-alpha");
|
|
94
|
+
const continueOptions = handlers.activeContinueOptions(state);
|
|
95
|
+
assert.deepStrictEqual(continueOptions, { provider: "codex", resumeSessionId: "codex-alpha" });
|
|
96
|
+
|
|
97
|
+
const ended = handlers.endCurrentSession({ state, persist: false });
|
|
98
|
+
assert.strictEqual(ended.ok, true);
|
|
99
|
+
assert.strictEqual(state.currentSession, null);
|
|
100
|
+
assert.strictEqual(stateApi.getProviderSession(state, "codex", "alpha"), "codex-alpha");
|
|
101
|
+
assert.strictEqual(stateApi.getProviderSession(state, "claude", "alpha"), "claude-alpha");
|
|
102
|
+
assert.strictEqual(stateApi.userOwnsProviderSession(state.userId, "alpha", "codex", "codex-alpha"), true);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function restartProbe() {
|
|
106
|
+
const state = stateApi.currentState();
|
|
107
|
+
state.currentSession = { name: "beta", dir: path.join(workspace, "beta") };
|
|
108
|
+
state.settings.backend = "codex";
|
|
109
|
+
stateApi.getProviderSettings(state, "claude").model = "claude-restored";
|
|
110
|
+
stateApi.getProviderSettings(state, "codex").model = "codex-restored";
|
|
111
|
+
stateApi.saveState({ strict: true });
|
|
112
|
+
|
|
113
|
+
const child = spawnSync(process.execPath, ["-e", [
|
|
114
|
+
"const s = require('./core/state');",
|
|
115
|
+
"const state = s.currentState();",
|
|
116
|
+
"process.stdout.write(JSON.stringify({",
|
|
117
|
+
" project: state.currentSession.name,",
|
|
118
|
+
" provider: s.getActiveProvider(state),",
|
|
119
|
+
" alphaClaude: s.getProviderSession(state, 'claude', 'alpha'),",
|
|
120
|
+
" alphaCodex: s.getProviderSession(state, 'codex', 'alpha'),",
|
|
121
|
+
" betaClaude: s.getProviderSession(state, 'claude', 'beta'),",
|
|
122
|
+
" betaCodex: s.getProviderSession(state, 'codex', 'beta'),",
|
|
123
|
+
" claudeModel: s.getProviderSettings(state, 'claude').model,",
|
|
124
|
+
" codexModel: s.getProviderSettings(state, 'codex').model,",
|
|
125
|
+
"}));",
|
|
126
|
+
].join("\n")], { cwd: root, env: process.env, encoding: "utf8" });
|
|
127
|
+
assert.strictEqual(child.status, 0, child.stderr || child.stdout);
|
|
128
|
+
assert.deepStrictEqual(JSON.parse(child.stdout), {
|
|
129
|
+
project: "beta",
|
|
130
|
+
provider: "codex",
|
|
131
|
+
alphaClaude: "claude-alpha",
|
|
132
|
+
alphaCodex: "codex-alpha",
|
|
133
|
+
betaClaude: "claude-beta",
|
|
134
|
+
betaCodex: "codex-beta",
|
|
135
|
+
claudeModel: "claude-restored",
|
|
136
|
+
codexModel: "codex-restored",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function commandRegistrationProbe() {
|
|
141
|
+
const names = new Set(commands.list().map((command) => command.name));
|
|
142
|
+
for (const name of ["new", "end", "continue", "backend", "claude", "codex"]) assert.ok(names.has(name), `/${name} registered`);
|
|
143
|
+
const source = fs.readFileSync(path.join(root, "core", "handlers.js"), "utf8");
|
|
144
|
+
assert.match(source, /name: "claude"[\s\S]*switchProviderCommand\(env, "claude"/);
|
|
145
|
+
assert.match(source, /name: "codex"[\s\S]*switchProviderCommand\(env, "codex"/);
|
|
146
|
+
assert.match(source, /name: "continue"[\s\S]*activeContinueOptions/);
|
|
147
|
+
const actions = fs.readFileSync(path.join(root, "core", "actions.js"), "utf8");
|
|
148
|
+
assert.match(actions, /switchProviderState/);
|
|
149
|
+
assert.match(actions, /startNewConversation/);
|
|
150
|
+
assert.match(actions, /endCurrentSession/);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async function removedProviderSelectionProbe() {
|
|
154
|
+
const state = stateApi.currentState();
|
|
155
|
+
state.currentSession = null;
|
|
156
|
+
state.settings.backend = null;
|
|
157
|
+
const sent = [];
|
|
158
|
+
await runInChat({
|
|
159
|
+
adapter: {
|
|
160
|
+
type: "fixture",
|
|
161
|
+
send: async (_channelId, text) => { sent.push(text); return true; },
|
|
162
|
+
},
|
|
163
|
+
channelId: "fixture",
|
|
164
|
+
canonicalUserId: state.userId,
|
|
165
|
+
userId: "fixture",
|
|
166
|
+
}, () => handlers.startSession("alpha"));
|
|
167
|
+
assert.strictEqual(state.settings.backend, null, "project selection cannot silently replace a removed provider");
|
|
168
|
+
assert.match(sent.join("\n"), /\/backend/);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function main() {
|
|
172
|
+
switchingAndProjectProbe();
|
|
173
|
+
newEndAndContinueProbe();
|
|
174
|
+
restartProbe();
|
|
175
|
+
commandRegistrationProbe();
|
|
176
|
+
await removedProviderSelectionProbe();
|
|
177
|
+
console.log("provider session commands OK");
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
main().catch((error) => {
|
|
181
|
+
console.error(error.stack || error.message);
|
|
182
|
+
process.exitCode = 1;
|
|
183
|
+
}).finally(() => {
|
|
184
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
185
|
+
});
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
"use strict";
|
|
4
|
+
|
|
5
|
+
const assert = require("assert");
|
|
6
|
+
const fs = require("fs");
|
|
7
|
+
const os = require("os");
|
|
8
|
+
const path = require("path");
|
|
9
|
+
|
|
10
|
+
const root = __dirname;
|
|
11
|
+
const configDir = fs.mkdtempSync(path.join(os.tmpdir(), "open-claudia-session-history-"));
|
|
12
|
+
process.env.OPEN_CLAUDIA_CONFIG_DIR = configDir;
|
|
13
|
+
process.env.TELEGRAM_CHAT_ID = "fixture";
|
|
14
|
+
process.env.OPEN_CLAUDIA_TEST = "1";
|
|
15
|
+
|
|
16
|
+
const state = require("./core/state");
|
|
17
|
+
const migration = require("./core/migration-backup");
|
|
18
|
+
|
|
19
|
+
function migrationProbe() {
|
|
20
|
+
const legacy = {
|
|
21
|
+
alpha: [
|
|
22
|
+
{ id: "legacy-a", title: "Legacy A", created: "2026-01-01T00:00:00.000Z", lastUsed: "2026-01-02T00:00:00.000Z" },
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
const migrated = state.migrateSessionsDocument(legacy, {
|
|
26
|
+
defaultUserId: "telegram:fixture",
|
|
27
|
+
canonicalizeUserId: (value) => String(value),
|
|
28
|
+
});
|
|
29
|
+
assert.strictEqual(migrated.schemaVersion, state.SESSIONS_SCHEMA_VERSION);
|
|
30
|
+
const legacyRecord = migrated.users["telegram:fixture"].alpha[0];
|
|
31
|
+
assert.strictEqual(legacyRecord.provider, "legacy");
|
|
32
|
+
assert.strictEqual(legacyRecord.project, "alpha");
|
|
33
|
+
assert.strictEqual(legacyRecord.selectable, false);
|
|
34
|
+
assert.deepStrictEqual(
|
|
35
|
+
state.migrateSessionsDocument(migrated, {
|
|
36
|
+
defaultUserId: "telegram:fixture",
|
|
37
|
+
canonicalizeUserId: (value) => String(value),
|
|
38
|
+
}),
|
|
39
|
+
migrated,
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const probed = state.migrateSessionsDocument(legacy, {
|
|
43
|
+
defaultUserId: "telegram:fixture",
|
|
44
|
+
canonicalizeUserId: (value) => String(value),
|
|
45
|
+
ownsSession(provider, context) {
|
|
46
|
+
return provider === "claude" && context.sessionId === "legacy-a";
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
assert.strictEqual(probed.users["telegram:fixture"].alpha[0].provider, "claude");
|
|
50
|
+
assert.strictEqual(probed.users["telegram:fixture"].alpha[0].selectable, true);
|
|
51
|
+
|
|
52
|
+
const ambiguous = state.migrateSessionsDocument(legacy, {
|
|
53
|
+
defaultUserId: "telegram:fixture",
|
|
54
|
+
canonicalizeUserId: (value) => String(value),
|
|
55
|
+
ownsSession: () => true,
|
|
56
|
+
});
|
|
57
|
+
assert.strictEqual(ambiguous.users["telegram:fixture"].alpha[0].provider, "legacy");
|
|
58
|
+
assert.strictEqual(ambiguous.users["telegram:fixture"].alpha[0].selectable, false);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function historyAndOwnershipProbe() {
|
|
62
|
+
const original = Buffer.from(JSON.stringify({
|
|
63
|
+
alpha: [{ id: "legacy-original", title: "Original" }],
|
|
64
|
+
}, null, 2));
|
|
65
|
+
fs.writeFileSync(path.join(configDir, "sessions.json"), original);
|
|
66
|
+
state.currentState();
|
|
67
|
+
state.saveState({ strict: true });
|
|
68
|
+
|
|
69
|
+
state.recordSession("owner@example.test", "alpha", "claude", "shared-id", "Claude Alpha", {
|
|
70
|
+
now: () => new Date("2026-02-01T00:00:00.000Z"),
|
|
71
|
+
strict: true,
|
|
72
|
+
});
|
|
73
|
+
state.recordSession("owner@example.test", "alpha", "codex", "shared-id", "Codex Alpha", {
|
|
74
|
+
now: () => new Date("2026-02-02T00:00:00.000Z"),
|
|
75
|
+
strict: true,
|
|
76
|
+
});
|
|
77
|
+
state.recordSession("owner@example.test", "beta", "claude", "shared-id", "Claude Beta", {
|
|
78
|
+
now: () => new Date("2026-02-03T00:00:00.000Z"),
|
|
79
|
+
strict: true,
|
|
80
|
+
});
|
|
81
|
+
state.recordSession("other@example.test", "alpha", "claude", "shared-id", "Other", {
|
|
82
|
+
now: () => new Date("2026-02-04T00:00:00.000Z"),
|
|
83
|
+
strict: true,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
assert.strictEqual(state.userOwnsProviderSession("owner@example.test", "alpha", "claude", "shared-id"), true);
|
|
87
|
+
assert.strictEqual(state.userOwnsProviderSession("owner@example.test", "alpha", "codex", "shared-id"), true);
|
|
88
|
+
assert.strictEqual(state.userOwnsProviderSession("owner@example.test", "beta", "codex", "shared-id"), false);
|
|
89
|
+
assert.strictEqual(state.userOwnsProviderSession("intruder@example.test", "alpha", "claude", "shared-id"), false);
|
|
90
|
+
|
|
91
|
+
assert.deepStrictEqual(
|
|
92
|
+
state.getProjectSessions("owner@example.test", "alpha", "claude").map((entry) => entry.title),
|
|
93
|
+
["Claude Alpha"],
|
|
94
|
+
);
|
|
95
|
+
assert.deepStrictEqual(
|
|
96
|
+
new Set(state.getProjectSessions("owner@example.test", "alpha").map((entry) => entry.provider)),
|
|
97
|
+
new Set(["claude", "codex"]),
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
state.recordSession("owner@example.test", "alpha", "claude", "older", "Older", {
|
|
101
|
+
now: () => new Date("2026-01-01T00:00:00.000Z"),
|
|
102
|
+
strict: true,
|
|
103
|
+
});
|
|
104
|
+
state.recordSession("owner@example.test", "alpha", "claude", "shared-id", null, {
|
|
105
|
+
now: () => new Date("2026-03-01T00:00:00.000Z"),
|
|
106
|
+
strict: true,
|
|
107
|
+
});
|
|
108
|
+
assert.strictEqual(state.getLastProjectSession("owner@example.test", "alpha", "claude").id, "shared-id");
|
|
109
|
+
|
|
110
|
+
const live = JSON.parse(fs.readFileSync(path.join(configDir, "sessions.json"), "utf8"));
|
|
111
|
+
assert.strictEqual(live.schemaVersion, state.SESSIONS_SCHEMA_VERSION);
|
|
112
|
+
assert.strictEqual(live.users["telegram:fixture"].alpha[0].provider, "legacy");
|
|
113
|
+
const journal = migration.readMigrationJournal(configDir);
|
|
114
|
+
assert.ok(journal.components.state);
|
|
115
|
+
assert.ok(journal.components.sessions);
|
|
116
|
+
const snapshotDir = path.join(migration.migrationRoot(configDir), ...journal.snapshot.split("/"));
|
|
117
|
+
assert.deepStrictEqual(fs.readFileSync(path.join(snapshotDir, "originals", "sessions.json")), original);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function callbackProbe() {
|
|
121
|
+
const { resolveSessionCallbackAction } = require("./core/actions");
|
|
122
|
+
const descriptor = {
|
|
123
|
+
userId: "owner@example.test",
|
|
124
|
+
project: "alpha",
|
|
125
|
+
provider: "claude",
|
|
126
|
+
sessionId: "shared-id",
|
|
127
|
+
};
|
|
128
|
+
const token = state.createSessionCallbackToken(descriptor, {
|
|
129
|
+
now: 1000,
|
|
130
|
+
ttlMs: 5000,
|
|
131
|
+
tokenFactory: () => "opaque-fixture-token",
|
|
132
|
+
});
|
|
133
|
+
assert.ok(`ss:${token}`.length <= 64);
|
|
134
|
+
assert.ok(!token.includes(descriptor.sessionId));
|
|
135
|
+
assert.strictEqual(state.consumeSessionCallbackToken(token, {
|
|
136
|
+
userId: descriptor.userId,
|
|
137
|
+
project: descriptor.project,
|
|
138
|
+
provider: descriptor.provider,
|
|
139
|
+
now: 2000,
|
|
140
|
+
}).status, "ok");
|
|
141
|
+
assert.strictEqual(state.consumeSessionCallbackToken(token, { ...descriptor, now: 2001 }).status, "expired");
|
|
142
|
+
|
|
143
|
+
const crossUser = state.createSessionCallbackToken(descriptor, {
|
|
144
|
+
now: 1000,
|
|
145
|
+
ttlMs: 5000,
|
|
146
|
+
tokenFactory: () => "cross-user-token",
|
|
147
|
+
});
|
|
148
|
+
assert.strictEqual(state.consumeSessionCallbackToken(crossUser, {
|
|
149
|
+
userId: "intruder@example.test",
|
|
150
|
+
project: "alpha",
|
|
151
|
+
provider: "claude",
|
|
152
|
+
now: 2000,
|
|
153
|
+
}).status, "forbidden");
|
|
154
|
+
|
|
155
|
+
const expired = state.createSessionCallbackToken(descriptor, {
|
|
156
|
+
now: 1000,
|
|
157
|
+
ttlMs: 5,
|
|
158
|
+
tokenFactory: () => "expired-token",
|
|
159
|
+
});
|
|
160
|
+
assert.strictEqual(state.consumeSessionCallbackToken(expired, { ...descriptor, now: 2000 }).status, "expired");
|
|
161
|
+
|
|
162
|
+
const liveState = {
|
|
163
|
+
userId: descriptor.userId,
|
|
164
|
+
currentSession: { name: descriptor.project, dir: "/workspace/alpha" },
|
|
165
|
+
settings: { backend: descriptor.provider },
|
|
166
|
+
};
|
|
167
|
+
const validAction = state.createSessionCallbackToken(descriptor, {
|
|
168
|
+
tokenFactory: () => "valid-action-token",
|
|
169
|
+
});
|
|
170
|
+
assert.strictEqual(resolveSessionCallbackAction(validAction, liveState).status, "ok");
|
|
171
|
+
|
|
172
|
+
const crossProviderAction = state.createSessionCallbackToken(descriptor, {
|
|
173
|
+
tokenFactory: () => "cross-provider-token",
|
|
174
|
+
});
|
|
175
|
+
assert.strictEqual(resolveSessionCallbackAction(crossProviderAction, {
|
|
176
|
+
...liveState,
|
|
177
|
+
settings: { backend: "codex" },
|
|
178
|
+
}).status, "forbidden");
|
|
179
|
+
|
|
180
|
+
const staleAction = state.createSessionCallbackToken({ ...descriptor, sessionId: "not-owned" }, {
|
|
181
|
+
tokenFactory: () => "stale-action-token",
|
|
182
|
+
});
|
|
183
|
+
assert.strictEqual(resolveSessionCallbackAction(staleAction, liveState).status, "stale");
|
|
184
|
+
assert.strictEqual(resolveSessionCallbackAction("shared-id", liveState).status, "expired", "raw native IDs are not callbacks");
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function integrationStaticProbe() {
|
|
188
|
+
const handlers = fs.readFileSync(path.join(root, "core", "handlers.js"), "utf8");
|
|
189
|
+
const actions = fs.readFileSync(path.join(root, "core", "actions.js"), "utf8");
|
|
190
|
+
assert.ok(!handlers.includes("callback_data: `ss:${s.id}`"));
|
|
191
|
+
assert.match(handlers, /createSessionCallbackToken/);
|
|
192
|
+
assert.match(handlers, /provider.*project|project.*provider/s);
|
|
193
|
+
assert.match(actions, /consumeSessionCallbackToken/);
|
|
194
|
+
assert.match(actions, /userOwnsProviderSession/);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
migrationProbe();
|
|
199
|
+
historyAndOwnershipProbe();
|
|
200
|
+
callbackProbe();
|
|
201
|
+
integrationStaticProbe();
|
|
202
|
+
console.log("provider session history OK");
|
|
203
|
+
} finally {
|
|
204
|
+
fs.rmSync(configDir, { recursive: true, force: true });
|
|
205
|
+
}
|