@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/bot.js
CHANGED
|
@@ -10,7 +10,31 @@ const path = require("path");
|
|
|
10
10
|
const fs = require("fs");
|
|
11
11
|
const { execSync } = require("child_process");
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const {
|
|
14
|
+
config, BOT_DIR, CONFIG_DIR, SOUL_FILE, VAULT_FILE, CHAT_ID,
|
|
15
|
+
STATE_FILE, SESSIONS_FILE, JOBS_FILE, CRONS_FILE,
|
|
16
|
+
ensureRuntimeDirectories,
|
|
17
|
+
} = require("./core/config");
|
|
18
|
+
ensureRuntimeDirectories();
|
|
19
|
+
const { defaultMigrationSources, ensureMigrationSnapshot } = require("./core/migration-backup");
|
|
20
|
+
const migrationSources = defaultMigrationSources({
|
|
21
|
+
configDir: CONFIG_DIR,
|
|
22
|
+
stateFile: STATE_FILE,
|
|
23
|
+
sessionsFile: SESSIONS_FILE,
|
|
24
|
+
jobsFile: JOBS_FILE,
|
|
25
|
+
cronsFile: CRONS_FILE,
|
|
26
|
+
});
|
|
27
|
+
const migrationSnapshot = ensureMigrationSnapshot({
|
|
28
|
+
configDir: CONFIG_DIR,
|
|
29
|
+
sources: migrationSources,
|
|
30
|
+
});
|
|
31
|
+
require("./core/provider-migration").completeProviderMigration({
|
|
32
|
+
configDir: CONFIG_DIR,
|
|
33
|
+
sources: migrationSources,
|
|
34
|
+
snapshot: migrationSnapshot,
|
|
35
|
+
});
|
|
36
|
+
const { readRuntimeMode, sideChatCoordinator } = require("./core/side-chat");
|
|
37
|
+
const runtimeMode = readRuntimeMode(CONFIG_DIR);
|
|
14
38
|
const { vault } = require("./core/vault-store");
|
|
15
39
|
const { isOnboarded } = require("./core/onboarding");
|
|
16
40
|
const { initScheduler, stopAll: stopScheduler } = require("./core/scheduler");
|
|
@@ -58,14 +82,22 @@ function persist() {
|
|
|
58
82
|
|
|
59
83
|
async function gracefulShutdown(signal) {
|
|
60
84
|
console.log(`Received ${signal}, shutting down gracefully...`);
|
|
85
|
+
try { stopScheduler(); } catch (e) {}
|
|
86
|
+
try { loopback.stop(); } catch (e) {}
|
|
87
|
+
const sideCleanup = sideChatCoordinator.cancelAll().catch(() => 0);
|
|
88
|
+
const memoryCleanup = require("./core/pack-review")
|
|
89
|
+
.drainReviews({ timeoutMs: 5000, cancelOnTimeout: true })
|
|
90
|
+
.catch((error) => {
|
|
91
|
+
console.error("memory shutdown drain failed:", error?.message);
|
|
92
|
+
return { drained: false };
|
|
93
|
+
});
|
|
61
94
|
for (const state of userStates.values()) {
|
|
62
95
|
if (state.runningProcess) {
|
|
63
96
|
try { killProcessTree(state.runningProcess.pid, "SIGTERM"); } catch (e) {}
|
|
64
97
|
}
|
|
65
98
|
}
|
|
99
|
+
await Promise.all([sideCleanup, memoryCleanup]);
|
|
66
100
|
for (const a of adapters) { try { await a.stop(); } catch (e) {} }
|
|
67
|
-
try { stopScheduler(); } catch (e) {}
|
|
68
|
-
try { loopback.stop(); } catch (e) {}
|
|
69
101
|
try {
|
|
70
102
|
const mediaDir = path.join(CONFIG_DIR, "media");
|
|
71
103
|
if (fs.existsSync(mediaDir)) {
|
|
@@ -234,6 +266,7 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
234
266
|
}
|
|
235
267
|
|
|
236
268
|
console.log(`Open Claudia v${CURRENT_VERSION} running on: ${adapters.map((a) => a.id).join(", ")}`);
|
|
269
|
+
console.log(`Runtime mode: ${runtimeMode} (single modular runtime)`);
|
|
237
270
|
console.log(`Workspace: ${require("./core/config").WORKSPACE}`);
|
|
238
271
|
console.log(`Soul: ${SOUL_FILE}`);
|
|
239
272
|
console.log(`Vault: ${VAULT_FILE} (${vault.exists() ? "exists" : "not created"})`);
|
|
@@ -251,6 +284,36 @@ setInterval(checkForUpdates, 5 * 60 * 1000);
|
|
|
251
284
|
}
|
|
252
285
|
} catch (e) {}
|
|
253
286
|
}
|
|
287
|
+
|
|
288
|
+
// Surface jobs the provider migration disabled. Announce once per job:
|
|
289
|
+
// marked only after a successful send, so a down adapter retries next boot.
|
|
290
|
+
try {
|
|
291
|
+
const jobsStore = require("./core/jobs");
|
|
292
|
+
const unannounced = jobsStore.listArchived().filter((job) => !job.announcedAt);
|
|
293
|
+
if (unannounced.length) {
|
|
294
|
+
const lines = unannounced.slice(0, 15).map((job) => {
|
|
295
|
+
const label = job.label || (job.prompt ? String(job.prompt).slice(0, 50) : job.id);
|
|
296
|
+
return `• [${job.kind || "job"}] ${label}\n ↳ ${job.archiveReason}`;
|
|
297
|
+
});
|
|
298
|
+
const message = [
|
|
299
|
+
`⚠️ ${unannounced.length} scheduled job(s) were disabled during the provider migration and will NOT fire:`,
|
|
300
|
+
"",
|
|
301
|
+
...lines,
|
|
302
|
+
unannounced.length > 15 ? `…and ${unannounced.length - 15} more.` : "",
|
|
303
|
+
"",
|
|
304
|
+
"Full list: open-claudia cron-list. Re-create the ones you still need with /cron add or schedule-wakeup.",
|
|
305
|
+
].filter(Boolean).join("\n");
|
|
306
|
+
const tg = adapters.find((a) => a.type === "telegram");
|
|
307
|
+
if (tg && CHAT_ID) {
|
|
308
|
+
await tg.send(CHAT_ID, message);
|
|
309
|
+
jobsStore.markArchivedAnnounced(unannounced.map((job) => job.id));
|
|
310
|
+
} else {
|
|
311
|
+
console.warn(message);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (e) {
|
|
315
|
+
console.error("archived-jobs notice failed:", e.message);
|
|
316
|
+
}
|
|
254
317
|
})().catch((e) => {
|
|
255
318
|
console.error("Boot failure:", e);
|
|
256
319
|
process.exit(1);
|
|
@@ -94,7 +94,12 @@ class TelegramAdapter {
|
|
|
94
94
|
process.exit(1);
|
|
95
95
|
}
|
|
96
96
|
try {
|
|
97
|
-
try {
|
|
97
|
+
try {
|
|
98
|
+
// `stopPolling()` without cancellation waits for the active getUpdates
|
|
99
|
+
// request to settle. A half-open socket may never settle, which wedges
|
|
100
|
+
// this recovery routine itself. Abort the request before restarting.
|
|
101
|
+
await this.bot.stopPolling({ cancel: true, reason: "stale Telegram long-poll recovery" });
|
|
102
|
+
} catch (e) {}
|
|
98
103
|
try { this._agent.destroy(); } catch (e) {} // drop any lingering pooled socket
|
|
99
104
|
await new Promise((r) => setTimeout(r, 500));
|
|
100
105
|
await this.bot.startPolling();
|
|
@@ -132,6 +132,7 @@ class VoiceAdapter {
|
|
|
132
132
|
// frontend, not just a voice channel.
|
|
133
133
|
if (pathname.startsWith("/v1/manage/")) {
|
|
134
134
|
return require("./manage").handle(req, res, url, {
|
|
135
|
+
canonicalUserId: canonicalForChannel("voice", this.channelId),
|
|
135
136
|
json: (code, payload) => this._json(res, code, payload),
|
|
136
137
|
readBody: (cb) => this._readBody(req, res, cb),
|
|
137
138
|
});
|
package/channels/voice/manage.js
CHANGED
|
@@ -24,7 +24,9 @@ const entities = require("../../core/entities");
|
|
|
24
24
|
const people = require("../../core/people");
|
|
25
25
|
const lessons = require("../../core/lessons");
|
|
26
26
|
const ideas = require("../../core/ideas");
|
|
27
|
-
const { TASKS_DIR,
|
|
27
|
+
const { TASKS_DIR, DEFAULT_PROVIDER } = require("../../core/config");
|
|
28
|
+
const { registry: providerRegistry } = require("../../core/providers");
|
|
29
|
+
const { getActiveProvider, getUserState } = require("../../core/state");
|
|
28
30
|
|
|
29
31
|
let PKG_VERSION = "unknown";
|
|
30
32
|
try { PKG_VERSION = require("../../package.json").version || "unknown"; } catch (e) {}
|
|
@@ -56,7 +58,25 @@ function isOpen(t) { return t.status === "pending" || t.status === "in_progress"
|
|
|
56
58
|
|
|
57
59
|
// ── responses ──────────────────────────────────────────────────────
|
|
58
60
|
|
|
59
|
-
function
|
|
61
|
+
function providerMetadata(providerId, source, reason = null) {
|
|
62
|
+
if (!providerId) return { providerId: null, label: null, model: null, defaultModel: null, source, reason };
|
|
63
|
+
try {
|
|
64
|
+
const provider = providerRegistry.getProvider(providerId);
|
|
65
|
+
const defaultModel = provider.defaultModel() || null;
|
|
66
|
+
return {
|
|
67
|
+
providerId: provider.id,
|
|
68
|
+
label: provider.label,
|
|
69
|
+
model: defaultModel,
|
|
70
|
+
defaultModel,
|
|
71
|
+
source,
|
|
72
|
+
reason,
|
|
73
|
+
};
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return { providerId, label: providerId, model: null, defaultModel: null, source, reason: error.message };
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function overview(canonicalUserId = null) {
|
|
60
80
|
const keys = taskFileKeys();
|
|
61
81
|
let openTasks = 0, inProgress = 0;
|
|
62
82
|
for (const key of keys) {
|
|
@@ -67,9 +87,27 @@ function overview() {
|
|
|
67
87
|
}
|
|
68
88
|
}
|
|
69
89
|
const allJobs = jobs.listAll();
|
|
90
|
+
const selectedDefault = providerRegistry.selectDefaultProvider(DEFAULT_PROVIDER);
|
|
91
|
+
const defaultProvider = providerMetadata(
|
|
92
|
+
selectedDefault.providerId,
|
|
93
|
+
selectedDefault.source,
|
|
94
|
+
selectedDefault.reason,
|
|
95
|
+
);
|
|
96
|
+
const activeState = canonicalUserId ? getUserState(canonicalUserId) : null;
|
|
97
|
+
const activeProviderId = getActiveProvider(activeState) || defaultProvider.providerId;
|
|
98
|
+
const activeProvider = providerMetadata(
|
|
99
|
+
activeProviderId,
|
|
100
|
+
getActiveProvider(activeState) ? "active" : defaultProvider.source,
|
|
101
|
+
activeProviderId ? null : defaultProvider.reason,
|
|
102
|
+
);
|
|
103
|
+
const activeModel = activeState?.providerSettings?.[activeProviderId]?.model || activeProvider.defaultModel;
|
|
104
|
+
activeProvider.model = activeModel || null;
|
|
70
105
|
return {
|
|
71
106
|
version: PKG_VERSION,
|
|
72
|
-
|
|
107
|
+
activeProvider,
|
|
108
|
+
defaultProvider,
|
|
109
|
+
// Compatibility for companion clients predating provider metadata.
|
|
110
|
+
model: activeProvider.model,
|
|
73
111
|
counts: {
|
|
74
112
|
tasksOpen: openTasks,
|
|
75
113
|
tasksInProgress: inProgress,
|
|
@@ -184,7 +222,7 @@ function ideasView() {
|
|
|
184
222
|
// we don't reach into its privates.
|
|
185
223
|
|
|
186
224
|
function handle(req, res, url, helpers) {
|
|
187
|
-
const { json, readBody } = helpers;
|
|
225
|
+
const { canonicalUserId, json, readBody } = helpers;
|
|
188
226
|
const method = req.method;
|
|
189
227
|
const sub = url.pathname.slice("/v1/manage/".length).replace(/\/+$/, "");
|
|
190
228
|
const parts = sub.split("/").filter(Boolean);
|
|
@@ -192,7 +230,7 @@ function handle(req, res, url, helpers) {
|
|
|
192
230
|
try {
|
|
193
231
|
// GET reads
|
|
194
232
|
if (method === "GET") {
|
|
195
|
-
if (sub === "overview") return json(200, { ok: true, ...overview() });
|
|
233
|
+
if (sub === "overview") return json(200, { ok: true, ...overview(canonicalUserId) });
|
|
196
234
|
if (sub === "tasks") return json(200, { ok: true, ...allTasks() });
|
|
197
235
|
if (sub === "jobs") return json(200, { ok: true, ...allJobsView() });
|
|
198
236
|
if (sub === "packs") return json(200, { ok: true, ...packSummaries() });
|
package/config-dir.js
CHANGED
|
@@ -1,11 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
const CONFIG_DIR = path.join(process.env.HOME || require("os").homedir(), ".open-claudia");
|
|
5
|
-
|
|
6
|
-
// Ensure directory exists
|
|
7
|
-
if (!fs.existsSync(CONFIG_DIR)) {
|
|
8
|
-
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
module.exports = CONFIG_DIR;
|
|
1
|
+
// Compatibility export for modules that need only the path. Resolving a path
|
|
2
|
+
// is intentionally pure: setup/runtime entrypoints create it explicitly.
|
|
3
|
+
module.exports = require("./core/config-dir").resolveConfigDir();
|
package/core/actions.js
CHANGED
|
@@ -4,11 +4,15 @@
|
|
|
4
4
|
|
|
5
5
|
const fs = require("fs");
|
|
6
6
|
const path = require("path");
|
|
7
|
-
const { CONFIG_DIR, WORKSPACE, CHAT_ID
|
|
7
|
+
const { CONFIG_DIR, WORKSPACE, CHAT_ID } = require("./config");
|
|
8
8
|
const { send } = require("./io");
|
|
9
|
-
const {
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
const {
|
|
10
|
+
currentState, saveState,
|
|
11
|
+
getActiveProvider, getProjectKey, consumeSessionCallbackToken,
|
|
12
|
+
userOwnsProviderSession,
|
|
13
|
+
} = require("./state");
|
|
14
|
+
const { admitRunContext, runAgent } = require("./runner");
|
|
15
|
+
const { listProjects, projectKeyboard } = require("./projects");
|
|
12
16
|
const { isChatOwner, approveAuthRequest, denyAuthRequest, authRequestLabel } = require("./access");
|
|
13
17
|
const accessStore = require("./access");
|
|
14
18
|
const peopleStore = require("./people");
|
|
@@ -16,13 +20,47 @@ const { vault } = require("./vault-store");
|
|
|
16
20
|
const intros = require("./intros");
|
|
17
21
|
const introFlow = require("./intro-flow");
|
|
18
22
|
const { finishOnboarding } = require("./onboarding");
|
|
19
|
-
const {
|
|
23
|
+
const {
|
|
24
|
+
startSession, switchProviderState, startNewConversation,
|
|
25
|
+
endCurrentSession, activeContinueOptions, applyProviderSetting,
|
|
26
|
+
} = require("./handlers");
|
|
27
|
+
const providerRegistry = require("./providers");
|
|
20
28
|
const jobs = require("./jobs");
|
|
21
29
|
const scheduler = require("./scheduler");
|
|
30
|
+
const { sideChatCoordinator } = require("./side-chat");
|
|
22
31
|
const {
|
|
23
32
|
getClaudeOAuthToken, runClaudeAuthCommand, runCodexLoginStatus, runCodexDeviceLogin,
|
|
24
33
|
} = require("./auth-flow");
|
|
25
34
|
|
|
35
|
+
const REMOVED_PROVIDER_CALLBACK_MESSAGE = "This button is from an older Open Claudia release. Cursor Agent support has been removed. Open /backend to choose Claude Code or OpenAI Codex.";
|
|
36
|
+
|
|
37
|
+
function isRemovedProviderCallback(payload) {
|
|
38
|
+
const value = String(payload || "");
|
|
39
|
+
return value === "be:cursor" || value === "mb:cursor" || value.startsWith("mb:cursor:");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function handleRemovedProviderCallback(payload, deliver = send) {
|
|
43
|
+
if (!isRemovedProviderCallback(payload)) return false;
|
|
44
|
+
await deliver(REMOVED_PROVIDER_CALLBACK_MESSAGE);
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveSessionCallbackAction(token, state, now = Date.now()) {
|
|
49
|
+
const expected = {
|
|
50
|
+
userId: state?.userId,
|
|
51
|
+
project: getProjectKey(state),
|
|
52
|
+
provider: getActiveProvider(state),
|
|
53
|
+
now,
|
|
54
|
+
};
|
|
55
|
+
const consumed = consumeSessionCallbackToken(token, expected);
|
|
56
|
+
if (consumed.status !== "ok") return consumed;
|
|
57
|
+
const value = consumed.value;
|
|
58
|
+
if (!userOwnsProviderSession(value.userId, value.project, value.provider, value.sessionId)) {
|
|
59
|
+
return { status: "stale", value: null };
|
|
60
|
+
}
|
|
61
|
+
return { status: "ok", value };
|
|
62
|
+
}
|
|
63
|
+
|
|
26
64
|
// Append an owner "always-allow" grant to a person's persona-pack Mandate so
|
|
27
65
|
// the guardrail won't escalate the same thing again. Wholesale-writes the
|
|
28
66
|
// combined mandate (upsert replaces the section); relationship is preserved.
|
|
@@ -46,11 +84,24 @@ function appendMandate(rec, line) {
|
|
|
46
84
|
async function handleAction(envelope) {
|
|
47
85
|
const adapter = envelope.adapter;
|
|
48
86
|
const d = envelope.action?.payload || envelope.action?.buttonId || "";
|
|
87
|
+
const state = currentState();
|
|
88
|
+
const continuePrompt = "continue";
|
|
89
|
+
const continuation = d === "a:continue" && state.currentSession ? activeContinueOptions(state) : null;
|
|
90
|
+
const continueRunContext = continuation
|
|
91
|
+
? admitRunContext(continuePrompt, state.currentSession.dir, null, continuation)
|
|
92
|
+
: null;
|
|
49
93
|
// Telegram needs us to ack the callback so the client stops spinning.
|
|
50
94
|
if (adapter && adapter.type === "telegram" && envelope.action?.callbackId) {
|
|
51
95
|
try { await adapter.bot.answerCallbackQuery(envelope.action.callbackId); } catch (e) {}
|
|
52
96
|
}
|
|
53
|
-
|
|
97
|
+
|
|
98
|
+
if (d.startsWith("sc:q:")) {
|
|
99
|
+
const outcome = await sideChatCoordinator.enqueue(d.slice("sc:q:".length), state.userId);
|
|
100
|
+
if (outcome.status === "queued") return send("Added to the main task queue.");
|
|
101
|
+
if (outcome.status === "forbidden") return send("That side-chat action belongs to another user.");
|
|
102
|
+
if (outcome.status === "expired") return send("That side-chat action expired. Send the message again if it still matters.");
|
|
103
|
+
return send(`Could not add that message to the main task: ${outcome.error?.message || "unknown error"}`);
|
|
104
|
+
}
|
|
54
105
|
|
|
55
106
|
if (d.startsWith("auth:")) {
|
|
56
107
|
if (!isChatOwner(envelope.channelId)) return send("Owner only — auth controls are restricted.");
|
|
@@ -233,47 +284,60 @@ async function handleAction(envelope) {
|
|
|
233
284
|
|
|
234
285
|
if (d === "show:projects") { await send("Pick:", { keyboard: projectKeyboard() }); return; }
|
|
235
286
|
if (d.startsWith("s:")) { startSession(d.slice(2)); return; }
|
|
236
|
-
if (d.startsWith("ss:")) {
|
|
287
|
+
if (d.startsWith("ss:")) {
|
|
288
|
+
const resolved = resolveSessionCallbackAction(d.slice(3), state);
|
|
289
|
+
if (resolved.status !== "ok") {
|
|
290
|
+
const message = resolved.status === "forbidden"
|
|
291
|
+
? "That conversation button belongs to another user, project, or provider."
|
|
292
|
+
: "That conversation button is stale or expired. Open /sessions again.";
|
|
293
|
+
return send(message);
|
|
294
|
+
}
|
|
295
|
+
return startSession(resolved.value.project, resolved.value.sessionId, { provider: resolved.value.provider });
|
|
296
|
+
}
|
|
237
297
|
if (d.startsWith("new:")) {
|
|
238
298
|
const proj = d.slice(4);
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
state.messageQueue = [];
|
|
243
|
-
resetSettings(state);
|
|
244
|
-
resetSessionUsage(state);
|
|
245
|
-
saveState();
|
|
299
|
+
const expected = proj === "__workspace__" ? "Workspace" : proj;
|
|
300
|
+
const result = startNewConversation({ state, projectName: expected });
|
|
301
|
+
if (!result.ok) return send("That new-conversation button is stale. Open /sessions again.");
|
|
246
302
|
await send(`Session: ${state.currentSession.name}\nNew conversation\n\nSend text, voice, or images.\n\n/sessions — switch conversation\n/session — switch project`);
|
|
247
303
|
return;
|
|
248
304
|
}
|
|
249
|
-
if (d === "a:continue") {
|
|
305
|
+
if (d === "a:continue") {
|
|
306
|
+
if (!continueRunContext) return send("No session to continue.");
|
|
307
|
+
return runAgent(continuePrompt, continueRunContext.cwd, null, {
|
|
308
|
+
runContext: continueRunContext,
|
|
309
|
+
...continuation,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
250
312
|
if (d === "a:end") {
|
|
251
313
|
if (state.currentSession) {
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
state.messageQueue = [];
|
|
256
|
-
resetSettings(state);
|
|
257
|
-
resetSessionUsage(state);
|
|
258
|
-
saveState();
|
|
259
|
-
await send(`Ended: ${n}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } });
|
|
314
|
+
const result = endCurrentSession({ state });
|
|
315
|
+
if (!result.ok) return send(`Could not end session: ${result.error.message}`);
|
|
316
|
+
await send(`Ended: ${result.project}`, { keyboard: { inline_keyboard: [[{ text: "New", callback_data: "show:projects" }]] } });
|
|
260
317
|
}
|
|
261
318
|
return;
|
|
262
319
|
}
|
|
263
320
|
if (d === "noop") return;
|
|
321
|
+
if (await handleRemovedProviderCallback(d)) return;
|
|
264
322
|
if (d.startsWith("mb:")) {
|
|
265
323
|
const rest = d.slice(3);
|
|
266
324
|
const colon = rest.indexOf(":");
|
|
267
325
|
if (colon < 0) return;
|
|
268
326
|
const be = rest.slice(0, colon);
|
|
269
327
|
const model = rest.slice(colon + 1);
|
|
270
|
-
|
|
271
|
-
|
|
328
|
+
let provider;
|
|
329
|
+
try { provider = providerRegistry.getProvider(be); }
|
|
330
|
+
catch (_) { return send("That provider is no longer available. Open /model again."); }
|
|
331
|
+
let available = false;
|
|
332
|
+
try { available = provider.isAvailable() === true; } catch (_) {}
|
|
333
|
+
if (!available) return send(`${provider.label} is unavailable. Run /doctor for recovery steps.`);
|
|
334
|
+
if (model !== "default" && !provider.modelChoices().includes(model)) {
|
|
335
|
+
return send("That model button is stale. Open /model again.");
|
|
336
|
+
}
|
|
272
337
|
const switched = state.settings.backend !== be;
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const beLabel = be === "cursor" ? "Cursor Agent" : be === "codex" ? "Codex" : "Claude Code";
|
|
338
|
+
const changed = switchProviderState(be, { state, model: model === "default" ? null : model });
|
|
339
|
+
if (!changed.ok) return send(`Could not switch provider: ${changed.error.message}`);
|
|
340
|
+
const beLabel = provider.label;
|
|
277
341
|
// Auth-aware: if the chosen provider isn't connected, start its login flow
|
|
278
342
|
// now. The model is already set, so once auth completes the user is on it.
|
|
279
343
|
if (be === "claude" && !getClaudeOAuthToken().value) {
|
|
@@ -291,9 +355,27 @@ async function handleAction(envelope) {
|
|
|
291
355
|
await send(switched ? `Switched to ${beLabel}.\nModel: ${model}` : `Model: ${model}`);
|
|
292
356
|
return;
|
|
293
357
|
}
|
|
294
|
-
if (d.startsWith("m:")) {
|
|
295
|
-
|
|
296
|
-
|
|
358
|
+
if (d.startsWith("m:")) {
|
|
359
|
+
state.settings.model = d.slice(2) === "default" ? null : d.slice(2);
|
|
360
|
+
await send(`Model: ${state.settings.model || "default"}`);
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (d.startsWith("e:")) {
|
|
364
|
+
const parts = d.split(":");
|
|
365
|
+
const providerId = parts.length >= 3 ? parts[1] : getActiveProvider(state);
|
|
366
|
+
const effort = parts.length >= 3 ? parts.slice(2).join(":") : parts[1];
|
|
367
|
+
const result = applyProviderSetting(state, providerId, "effort", effort, { requireActive: true, persist: true });
|
|
368
|
+
await send(result.ok ? `${result.providerLabel} effort: ${result.value || "default"}` : result.message);
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
if (d.startsWith("b:")) {
|
|
372
|
+
const parts = d.split(":");
|
|
373
|
+
const providerId = parts.length >= 3 ? parts[1] : getActiveProvider(state);
|
|
374
|
+
const budget = parts.length >= 3 ? parts.slice(2).join(":") : parts[1];
|
|
375
|
+
const result = applyProviderSetting(state, providerId, "budget", budget, { requireActive: true, persist: true });
|
|
376
|
+
await send(result.ok ? `${result.providerLabel} budget: ${result.value ? "$" + result.value : "none"}` : result.message);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
297
379
|
if (d.startsWith("eng:")) {
|
|
298
380
|
const v = d.slice(4);
|
|
299
381
|
const recall = require("./recall");
|
|
@@ -386,6 +468,9 @@ async function handleAction(envelope) {
|
|
|
386
468
|
: `[Tool approval ${rec.id}] The owner DENIED the held run${isExternalAction ? ` requested by ${rec.personName || "an external contact"}` : ""}:\n${rec.command}\n` +
|
|
387
469
|
`Do NOT run it. ${isExternalAction ? "Let them know you can't do that one and move on." : "Acknowledge the denial to the user and adjust course."}`;
|
|
388
470
|
try {
|
|
471
|
+
if (!rec.provider || !rec.project || !rec.canonicalUserId) {
|
|
472
|
+
throw new Error("the original provider/project context predates safe scheduled-run pinning");
|
|
473
|
+
}
|
|
389
474
|
scheduler.addJob({
|
|
390
475
|
kind: "wakeup",
|
|
391
476
|
// External escalations carry the origin as an adapter TYPE string
|
|
@@ -393,6 +478,20 @@ async function handleAction(envelope) {
|
|
|
393
478
|
adapter: isExternalAction ? "" : (rec.adapter || (adapter && adapter.id) || ""),
|
|
394
479
|
adapterType: isExternalAction ? (rec.adapter || (adapter && adapter.type)) : (adapter && adapter.type),
|
|
395
480
|
channelId: String(rec.channelId || envelope.channelId),
|
|
481
|
+
canonicalUserId: rec.canonicalUserId,
|
|
482
|
+
userId: rec.userId || null,
|
|
483
|
+
provider: rec.provider,
|
|
484
|
+
providerSettings: rec.providerSettings || {},
|
|
485
|
+
project: rec.project,
|
|
486
|
+
projectDir: rec.projectDir || null,
|
|
487
|
+
sessionId: rec.sessionId || null,
|
|
488
|
+
previousSessionId: rec.previousSessionId || null,
|
|
489
|
+
originRunId: rec.originRunId || null,
|
|
490
|
+
sessionLineage: {
|
|
491
|
+
originRunId: rec.originRunId || null,
|
|
492
|
+
originSessionId: rec.sessionId || null,
|
|
493
|
+
previousSessionId: rec.previousSessionId || null,
|
|
494
|
+
},
|
|
396
495
|
prompt,
|
|
397
496
|
label: `Tool approval ${rec.status}: ${label}`.slice(0, 60),
|
|
398
497
|
source: "approval",
|
|
@@ -432,22 +531,26 @@ async function handleAction(envelope) {
|
|
|
432
531
|
}
|
|
433
532
|
if (d.startsWith("be:")) {
|
|
434
533
|
const be = d.slice(3);
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
534
|
+
let provider;
|
|
535
|
+
try { provider = providerRegistry.getProvider(be); }
|
|
536
|
+
catch (_) { return send("That provider is no longer available. Open /backend again."); }
|
|
537
|
+
let available = false;
|
|
538
|
+
try { available = provider.isAvailable() === true; } catch (_) {}
|
|
539
|
+
if (!available) return send(`${provider.label} is unavailable. Run /doctor for recovery steps.`);
|
|
540
|
+
const switched = switchProviderState(be, { state });
|
|
541
|
+
if (!switched.ok) return send(`Could not switch provider: ${switched.error.message}`);
|
|
542
|
+
await send(`Switched to ${provider.label}.`);
|
|
442
543
|
return;
|
|
443
544
|
}
|
|
444
545
|
|
|
445
546
|
if (d.startsWith("mode:")) {
|
|
446
547
|
if (!isChatOwner(envelope.channelId)) { await send("Owner only — mode switch restarts the bot."); return; }
|
|
447
548
|
const newMode = d.slice(5);
|
|
549
|
+
if (!new Set(["direct", "agent"]).has(newMode)) return send("Unknown runtime mode.");
|
|
448
550
|
const modeFile = path.join(CONFIG_DIR, ".bot-mode");
|
|
449
551
|
fs.writeFileSync(modeFile, newMode);
|
|
450
552
|
await send(`Switching to ${newMode} mode... restarting.`);
|
|
553
|
+
await sideChatCoordinator.cancelAll();
|
|
451
554
|
setTimeout(() => process.exit(0), 500);
|
|
452
555
|
return;
|
|
453
556
|
}
|
|
@@ -499,4 +602,10 @@ async function handleAction(envelope) {
|
|
|
499
602
|
}
|
|
500
603
|
}
|
|
501
604
|
|
|
502
|
-
module.exports = {
|
|
605
|
+
module.exports = {
|
|
606
|
+
REMOVED_PROVIDER_CALLBACK_MESSAGE,
|
|
607
|
+
handleAction,
|
|
608
|
+
handleRemovedProviderCallback,
|
|
609
|
+
isRemovedProviderCallback,
|
|
610
|
+
resolveSessionCallbackAction,
|
|
611
|
+
};
|