@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.
Files changed (157) hide show
  1. package/.env.example +30 -4
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +87 -66
  4. package/bin/agent.js +44 -18
  5. package/bin/cli.js +30 -28
  6. package/bin/dream.js +5 -11
  7. package/bin/loopback-client.js +29 -5
  8. package/bin/schedule.js +19 -5
  9. package/bin/tool.js +23 -5
  10. package/bot-agent.js +5 -2350
  11. package/bot.js +105 -7
  12. package/channels/telegram/adapter.js +6 -1
  13. package/channels/voice/adapter.js +1 -0
  14. package/channels/voice/manage.js +43 -5
  15. package/config-dir.js +3 -11
  16. package/core/actions.js +149 -40
  17. package/core/approvals.js +136 -29
  18. package/core/auth-flow.js +103 -19
  19. package/core/config-dir.js +19 -0
  20. package/core/config.js +127 -69
  21. package/core/doctor.js +111 -57
  22. package/core/dream.js +219 -62
  23. package/core/enforcer.js +0 -0
  24. package/core/entities.js +2 -1
  25. package/core/fsutil.js +55 -0
  26. package/core/handlers.js +503 -217
  27. package/core/ideas.js +2 -1
  28. package/core/identity.js +8 -11
  29. package/core/io.js +24 -3
  30. package/core/jobs.js +589 -72
  31. package/core/lessons.js +3 -2
  32. package/core/loopback.js +45 -8
  33. package/core/memory-mutation-queue.js +241 -0
  34. package/core/migration-backup.js +1060 -0
  35. package/core/pack-review.js +295 -88
  36. package/core/packs.js +4 -3
  37. package/core/persona.js +2 -1
  38. package/core/process-tree.js +19 -2
  39. package/core/provider-migration.js +39 -0
  40. package/core/provider-status.js +80 -0
  41. package/core/providers/claude-events.js +117 -0
  42. package/core/providers/claude-hook.js +114 -0
  43. package/core/providers/claude.js +296 -0
  44. package/core/providers/codex-events.js +121 -0
  45. package/core/providers/codex-hook.js +280 -0
  46. package/core/providers/codex.js +286 -0
  47. package/core/providers/contract.js +153 -0
  48. package/core/providers/env.js +148 -0
  49. package/core/providers/events.js +170 -0
  50. package/core/providers/hook-command.js +22 -0
  51. package/core/providers/index.js +240 -0
  52. package/core/providers/utility-policy.js +269 -0
  53. package/core/recall/classic.js +2 -2
  54. package/core/recall/discoverer.js +71 -22
  55. package/core/recall/metrics.js +27 -1
  56. package/core/recall/tuning.js +4 -1
  57. package/core/recall/warm-walker.js +151 -108
  58. package/core/recall-filter.js +86 -61
  59. package/core/router.js +79 -7
  60. package/core/run-context.js +185 -0
  61. package/core/runner.js +1415 -1282
  62. package/core/scheduler.js +515 -210
  63. package/core/side-chat.js +346 -0
  64. package/core/state.js +1096 -95
  65. package/core/subagent.js +72 -98
  66. package/core/system-prompt.js +462 -279
  67. package/core/tool-guard.js +73 -39
  68. package/core/tools.js +22 -5
  69. package/core/transcripts.js +30 -1
  70. package/core/usage-log.js +19 -4
  71. package/core/utility-agent.js +654 -0
  72. package/core/web-auth.js +29 -13
  73. package/core/web-sessions.js +78 -0
  74. package/docs/CHANNEL_DESIGN.md +181 -0
  75. package/docs/MULTI_CHANNEL_PLAN.md +254 -0
  76. package/docs/PROVIDER_MIGRATION.md +67 -0
  77. package/health.js +50 -39
  78. package/package.json +51 -4
  79. package/setup.js +198 -38
  80. package/soul.md +39 -0
  81. package/test-ability-extraction.js +5 -0
  82. package/test-approval-async.js +63 -4
  83. package/test-cursor-removal.js +334 -0
  84. package/test-delivery-contract.js +88 -0
  85. package/test-enforcer.js +70 -27
  86. package/test-fixtures/current-provider-parity.json +132 -0
  87. package/test-fixtures/fake-agent-cli.js +477 -0
  88. package/test-fixtures/migrations/claude-only/jobs.json +3 -0
  89. package/test-fixtures/migrations/claude-only/sessions.json +5 -0
  90. package/test-fixtures/migrations/claude-only/state.json +5 -0
  91. package/test-fixtures/migrations/cursor-selected/crons.json +3 -0
  92. package/test-fixtures/migrations/cursor-selected/jobs.json +3 -0
  93. package/test-fixtures/migrations/cursor-selected/sessions.json +5 -0
  94. package/test-fixtures/migrations/cursor-selected/state.json +6 -0
  95. package/test-fixtures/migrations/dual-provider/jobs.json +3 -0
  96. package/test-fixtures/migrations/dual-provider/sessions.json +7 -0
  97. package/test-fixtures/migrations/dual-provider/state.json +10 -0
  98. package/test-fixtures/migrations/malformed-partial/jobs.json +1 -0
  99. package/test-fixtures/migrations/malformed-partial/sessions.json +1 -0
  100. package/test-fixtures/migrations/malformed-partial/sessions.json.bak +3 -0
  101. package/test-fixtures/migrations/malformed-partial/state.json +1 -0
  102. package/test-fixtures/migrations/malformed-partial/state.json.bak +5 -0
  103. package/test-fixtures/migrations/missing-project/jobs.json +3 -0
  104. package/test-fixtures/migrations/missing-project/sessions.json +3 -0
  105. package/test-fixtures/migrations/missing-project/state.json +4 -0
  106. package/test-fixtures/migrations/multi-user/jobs.json +4 -0
  107. package/test-fixtures/migrations/multi-user/sessions.json +4 -0
  108. package/test-fixtures/migrations/multi-user/state.json +6 -0
  109. package/test-fixtures/provider-event-samples.json +17 -0
  110. package/test-learning-e2e.js +5 -0
  111. package/test-memory-mutation-queue.js +191 -0
  112. package/test-provider-boot-matrix.js +399 -0
  113. package/test-provider-bootstrap.js +158 -0
  114. package/test-provider-capabilities.js +232 -0
  115. package/test-provider-claude.js +206 -0
  116. package/test-provider-codex.js +228 -0
  117. package/test-provider-compaction.js +371 -0
  118. package/test-provider-core-prompt.js +264 -0
  119. package/test-provider-coupling-audit.js +90 -0
  120. package/test-provider-dream.js +312 -0
  121. package/test-provider-enforcer.js +252 -0
  122. package/test-provider-env-isolation.js +150 -0
  123. package/test-provider-events.js +141 -0
  124. package/test-provider-fixture.js +332 -0
  125. package/test-provider-gateway.js +508 -0
  126. package/test-provider-language.js +89 -0
  127. package/test-provider-migration-backup.js +424 -0
  128. package/test-provider-pack-review.js +436 -0
  129. package/test-provider-parity-e2e.js +537 -0
  130. package/test-provider-prompt-parity.js +89 -0
  131. package/test-provider-recall.js +271 -0
  132. package/test-provider-registry.js +251 -0
  133. package/test-provider-resume-parity.js +41 -0
  134. package/test-provider-run-lifecycle.js +349 -0
  135. package/test-provider-runner.js +271 -0
  136. package/test-provider-scheduler.js +689 -0
  137. package/test-provider-session-commands.js +185 -0
  138. package/test-provider-session-history.js +205 -0
  139. package/test-provider-side-chat.js +337 -0
  140. package/test-provider-state-migration.js +316 -0
  141. package/test-provider-stream-decoder.js +69 -0
  142. package/test-provider-subagent.js +228 -0
  143. package/test-provider-tool-hooks.js +360 -0
  144. package/test-recall-discoverer.js +1 -0
  145. package/test-recall-engine.js +3 -3
  146. package/test-recall-evolution.js +18 -0
  147. package/test-run-lock.js +63 -0
  148. package/test-runner-watchdog-static.js +16 -8
  149. package/test-single-runtime.js +35 -0
  150. package/test-telegram-poll-recovery.js +56 -0
  151. package/test-tool-guard.js +56 -0
  152. package/test-tools.js +19 -0
  153. package/test-unified-identity.js +3 -1
  154. package/test-usage-accounting.js +92 -20
  155. package/test-utility-provider-policy.js +486 -0
  156. package/test-web-sessions.js +74 -0
  157. 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
+ }