@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
package/core/recall-filter.js
CHANGED
|
@@ -1,18 +1,16 @@
|
|
|
1
|
-
|
|
2
|
-
// packs.js/entities.js are high-recall but dumb — "zoom" pulls in the
|
|
3
|
-
// video-gen pack, "kazee" pulls in every colleague. When keywords produce
|
|
4
|
-
// candidates, a cheap model judges which are genuinely about the message.
|
|
5
|
-
// Fail-open: any error, timeout, or unparseable reply keeps all candidates,
|
|
6
|
-
// so recall never gets worse than the keyword baseline.
|
|
1
|
+
"use strict";
|
|
7
2
|
|
|
8
|
-
|
|
3
|
+
// LLM relevance gate over keyword recall candidates. Fail-open is deliberate:
|
|
4
|
+
// an unavailable provider, timeout, or malformed judgement retains the
|
|
5
|
+
// keyword baseline, so intelligence failure never makes classic recall worse.
|
|
9
6
|
|
|
10
|
-
const FILTER_MODEL = process.env.RECALL_FILTER_MODEL || "haiku";
|
|
11
7
|
const FILTER_TIMEOUT_MS = Number(process.env.RECALL_FILTER_TIMEOUT_MS || 20000);
|
|
12
8
|
const MESSAGE_CLIP_CHARS = 1500;
|
|
13
|
-
// Quote + a window of recent turns: roomier than the message clip so the
|
|
14
|
-
// newest history lines at the tail don't get sliced off.
|
|
15
9
|
const CONTEXT_CLIP_CHARS = 3500;
|
|
10
|
+
const FILTER_OUTPUT_SCHEMA = Object.freeze({
|
|
11
|
+
type: "array",
|
|
12
|
+
items: { type: "string" },
|
|
13
|
+
});
|
|
16
14
|
|
|
17
15
|
function enabled() {
|
|
18
16
|
return (process.env.RECALL_FILTER || "on").toLowerCase() !== "off";
|
|
@@ -23,57 +21,84 @@ const FILTER_SYSTEM_PROMPT = [
|
|
|
23
21
|
"Reply with ONLY a JSON array of candidate ids — no prose, no markdown fences.",
|
|
24
22
|
].join("\n");
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
24
|
+
function parseIds(result) {
|
|
25
|
+
if (Array.isArray(result?.json)) return result.json;
|
|
26
|
+
const match = String(result?.text || "").match(/\[[\s\S]*?\]/);
|
|
27
|
+
if (!match) return null;
|
|
28
|
+
const parsed = JSON.parse(match[0]);
|
|
29
|
+
return Array.isArray(parsed) ? parsed : null;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function createRecallFilter(dependencies = {}) {
|
|
33
|
+
const runUtility = dependencies.spawnUtilityAgent || require("./utility-agent").spawnUtilityAgent;
|
|
34
|
+
|
|
35
|
+
async function judgeRelevance(message, candidates, opts = {}) {
|
|
36
|
+
if (!enabled() || candidates.length === 0) return null;
|
|
37
|
+
const msg = String(message || "").slice(0, MESSAGE_CLIP_CHARS);
|
|
38
|
+
const context = String(opts.context || "").slice(0, CONTEXT_CLIP_CHARS);
|
|
39
|
+
const lines = candidates.map((candidate) => (
|
|
40
|
+
`- ${candidate.id}: ${candidate.name}${candidate.description ? ` — ${candidate.description}` : ""}`
|
|
41
|
+
));
|
|
42
|
+
const prompt = [
|
|
43
|
+
"A user sent this message to their assistant:",
|
|
44
|
+
"<<<",
|
|
45
|
+
msg,
|
|
46
|
+
">>>",
|
|
47
|
+
...(context
|
|
48
|
+
? [
|
|
49
|
+
"",
|
|
50
|
+
"Surrounding conversation (the message it replied to and/or the most recent turns in this chat — context for resolving references like \"the app\", \"it\", or terse follow-ups like \"push\"; judge relevance against what the user's message is about in light of it):",
|
|
51
|
+
"<<<",
|
|
52
|
+
context,
|
|
53
|
+
">>>",
|
|
54
|
+
]
|
|
55
|
+
: []),
|
|
56
|
+
"",
|
|
57
|
+
"These stored memory notes were keyword-matched as possibly relevant.",
|
|
58
|
+
"Keep only the ones the message is GENUINELY about — the topic, project, or person itself.",
|
|
59
|
+
"Sharing an incidental word with the note (e.g. \"zoom\" matching a video tool, a person's employer matching a project name) is NOT relevance.",
|
|
60
|
+
"If the conversation context shows the user means a different project than a candidate (e.g. they are discussing app A, and a candidate is the unrelated app B that merely shares words like \"mobile\" or \"gradient\"), drop that candidate.",
|
|
61
|
+
"",
|
|
62
|
+
"Candidates:",
|
|
63
|
+
...lines,
|
|
64
|
+
"",
|
|
65
|
+
`Reply with ONLY a JSON array of the relevant ids, e.g. ["${candidates[0].id}"]. Use [] if none are relevant.`,
|
|
66
|
+
].join("\n");
|
|
67
|
+
try {
|
|
68
|
+
const result = await runUtility(prompt, {
|
|
69
|
+
purpose: "recall",
|
|
70
|
+
activeProvider: opts.provider || null,
|
|
71
|
+
tier: "low",
|
|
72
|
+
model: opts.model || null,
|
|
73
|
+
modelEnvPrefix: "RECALL_FILTER_MODEL",
|
|
74
|
+
legacyClaudeModelEnv: "RECALL_FILTER_MODEL",
|
|
75
|
+
systemPrompt: FILTER_SYSTEM_PROMPT,
|
|
76
|
+
readOnly: true,
|
|
77
|
+
outputSchema: FILTER_OUTPUT_SCHEMA,
|
|
78
|
+
timeoutMs: FILTER_TIMEOUT_MS,
|
|
79
|
+
cwd: opts.cwd || process.cwd(),
|
|
80
|
+
});
|
|
81
|
+
const ids = parseIds(result);
|
|
82
|
+
if (!ids) return null;
|
|
83
|
+
const known = new Set(candidates.map((candidate) => candidate.id));
|
|
84
|
+
return new Set(ids.filter((id) => typeof id === "string" && known.has(id)));
|
|
85
|
+
} catch (_) {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
76
88
|
}
|
|
89
|
+
|
|
90
|
+
return { judgeRelevance };
|
|
77
91
|
}
|
|
78
92
|
|
|
79
|
-
|
|
93
|
+
const defaultFilter = createRecallFilter();
|
|
94
|
+
|
|
95
|
+
module.exports = {
|
|
96
|
+
CONTEXT_CLIP_CHARS,
|
|
97
|
+
FILTER_OUTPUT_SCHEMA,
|
|
98
|
+
FILTER_SYSTEM_PROMPT,
|
|
99
|
+
FILTER_TIMEOUT_MS,
|
|
100
|
+
MESSAGE_CLIP_CHARS,
|
|
101
|
+
createRecallFilter,
|
|
102
|
+
enabled,
|
|
103
|
+
judgeRelevance: defaultFilter.judgeRelevance,
|
|
104
|
+
};
|
package/core/router.js
CHANGED
|
@@ -11,7 +11,7 @@ const { isChatAuthorized } = require("./access");
|
|
|
11
11
|
const introFlow = require("./intro-flow");
|
|
12
12
|
const { send, deleteMessage } = require("./io");
|
|
13
13
|
const { currentState, autoLinkOwnerChannel } = require("./state");
|
|
14
|
-
const {
|
|
14
|
+
const { admitRunContext, runAgent } = require("./runner");
|
|
15
15
|
const { transcribeAudio } = require("./media");
|
|
16
16
|
const { vault } = require("./vault-store");
|
|
17
17
|
const { isOnboarded, handleOnboarding } = require("./onboarding");
|
|
@@ -26,6 +26,21 @@ const { MAX_FILE_SIZE, MAX_VOICE_SIZE, FILES_DIR } = require("./config");
|
|
|
26
26
|
const channelWizard = require("./channel-wizard");
|
|
27
27
|
require("./handlers"); // side-effect: register slash commands
|
|
28
28
|
|
|
29
|
+
const USER_VISIBLE_PROVIDER_ERRORS = new Set([
|
|
30
|
+
"NO_AVAILABLE_PROVIDER",
|
|
31
|
+
"PROVIDER_UNAVAILABLE",
|
|
32
|
+
"PROVIDER_INCOMPATIBLE",
|
|
33
|
+
"PROVIDER_UNAUTHENTICATED",
|
|
34
|
+
]);
|
|
35
|
+
|
|
36
|
+
async function deliverRouterError(error) {
|
|
37
|
+
const code = error?.code || "ROUTER_ERROR";
|
|
38
|
+
if (!USER_VISIBLE_PROVIDER_ERRORS.has(code)) return null;
|
|
39
|
+
const message = error?.message || "No compatible provider is available.";
|
|
40
|
+
await send(`${message}\n\nOpen /backend to review providers, or run /doctor for local compatibility and authentication checks.`);
|
|
41
|
+
return { ok: false, status: "failed", error: { code, message } };
|
|
42
|
+
}
|
|
43
|
+
|
|
29
44
|
// Cross-channel dedup. Message ids are unique per adapter, not globally, so
|
|
30
45
|
// the adapter stays in the key. Keying by canonical (not raw channelId) means
|
|
31
46
|
// a message redelivered against a different channel that resolves to the same
|
|
@@ -109,6 +124,8 @@ async function onMessage(envelope) {
|
|
|
109
124
|
if (envelope.type === "text") return handleText(envelope);
|
|
110
125
|
} catch (e) {
|
|
111
126
|
console.error("router:onMessage:", e.message);
|
|
127
|
+
const delivered = await deliverRouterError(e);
|
|
128
|
+
if (delivered) return delivered;
|
|
112
129
|
}
|
|
113
130
|
});
|
|
114
131
|
}
|
|
@@ -133,6 +150,10 @@ async function handleVoice(envelope) {
|
|
|
133
150
|
if (media.size && media.size > MAX_VOICE_SIZE) {
|
|
134
151
|
return send(`Voice note too large (${Math.round(media.size / 1024 / 1024)}MB). Max: ${MAX_VOICE_SIZE / 1024 / 1024}MB`);
|
|
135
152
|
}
|
|
153
|
+
const runContext = admitRunContext("", state.currentSession.dir, envelope.messageId, {
|
|
154
|
+
purpose: "foreground",
|
|
155
|
+
attachments: [{ type: "voice", mimeType: media.mimeType || "audio/ogg", fileName: media.fileName || null }],
|
|
156
|
+
});
|
|
136
157
|
envelope.adapter.typing(envelope.channelId).catch(() => {});
|
|
137
158
|
const tStart = Date.now();
|
|
138
159
|
const oggPath = await envelope.adapter.downloadMedia(media);
|
|
@@ -145,7 +166,12 @@ async function handleVoice(envelope) {
|
|
|
145
166
|
state.lastInputWasVoice = true;
|
|
146
167
|
// Per-stage latency for the voice pipeline; the runner fills in the rest.
|
|
147
168
|
state.voiceLat = { startAt: tStart, downloadMs: tAfterDownload - tStart, sttMs: tAfterStt - tAfterDownload };
|
|
148
|
-
|
|
169
|
+
return runAgent(transcript, runContext.cwd, envelope.messageId, {
|
|
170
|
+
runContext,
|
|
171
|
+
attachments: [{ type: "voice", path: oggPath, mimeType: media.mimeType || "audio/ogg", fileName: media.fileName || null }],
|
|
172
|
+
lastInputWasVoice: true,
|
|
173
|
+
voiceStream: envelope.adapter?.type === "voice",
|
|
174
|
+
});
|
|
149
175
|
} catch (err) { await send(`Voice failed: ${err.message}`); }
|
|
150
176
|
}
|
|
151
177
|
|
|
@@ -156,13 +182,20 @@ async function handleAudio(envelope) {
|
|
|
156
182
|
try {
|
|
157
183
|
const media = envelope.media?.[0];
|
|
158
184
|
if (!media) return;
|
|
185
|
+
const runContext = admitRunContext("", state.currentSession.dir, envelope.messageId, {
|
|
186
|
+
purpose: "foreground",
|
|
187
|
+
attachments: [{ type: "audio", mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
188
|
+
});
|
|
159
189
|
envelope.adapter.typing(envelope.channelId).catch(() => {});
|
|
160
190
|
const p = await envelope.adapter.downloadMedia(media);
|
|
161
191
|
const t = transcribeAudio(p);
|
|
162
192
|
try { fs.unlinkSync(p); } catch (e) {}
|
|
163
193
|
if (!t) return send("Couldn't transcribe.");
|
|
164
194
|
await send(`Heard: "${t}"`, { replyTo: envelope.messageId });
|
|
165
|
-
|
|
195
|
+
return runAgent(t, runContext.cwd, envelope.messageId, {
|
|
196
|
+
runContext,
|
|
197
|
+
attachments: [{ type: "audio", path: p, mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
198
|
+
});
|
|
166
199
|
} catch (err) { await send(`Audio failed: ${err.message}`); }
|
|
167
200
|
}
|
|
168
201
|
|
|
@@ -173,13 +206,26 @@ async function handlePhoto(envelope) {
|
|
|
173
206
|
try {
|
|
174
207
|
const items = envelope.media || [];
|
|
175
208
|
if (!items.length) return;
|
|
209
|
+
const runContext = admitRunContext(envelope.caption || "", state.currentSession.dir, envelope.messageId, {
|
|
210
|
+
purpose: "foreground",
|
|
211
|
+
attachments: items.map((item) => ({ type: "image", mimeType: item.mimeType || "image/*", fileName: item.fileName || null })),
|
|
212
|
+
});
|
|
176
213
|
const paths = [];
|
|
177
214
|
for (const m of items) paths.push(await envelope.adapter.downloadMedia(m));
|
|
178
215
|
const caption = envelope.caption || "Describe this image. If code/UI/error — explain and fix.";
|
|
179
216
|
const prompt = paths.length === 1
|
|
180
217
|
? `Image at ${paths[0]}\n\nView it, then: ${caption}`
|
|
181
218
|
: `Images:\n${paths.map((p, i) => `${i + 1}. ${p}`).join("\n")}\n\nView all ${paths.length}, then: ${caption}`;
|
|
182
|
-
|
|
219
|
+
return runAgent(prompt, runContext.cwd, envelope.messageId, {
|
|
220
|
+
runContext,
|
|
221
|
+
imagePaths: paths,
|
|
222
|
+
attachments: paths.map((imagePath, index) => ({
|
|
223
|
+
type: "image",
|
|
224
|
+
path: imagePath,
|
|
225
|
+
mimeType: items[index]?.mimeType || "image/*",
|
|
226
|
+
fileName: items[index]?.fileName || path.basename(imagePath),
|
|
227
|
+
})),
|
|
228
|
+
});
|
|
183
229
|
} catch (err) { await send(`Image failed: ${err.message}`); }
|
|
184
230
|
}
|
|
185
231
|
|
|
@@ -193,6 +239,10 @@ async function handleDocument(envelope) {
|
|
|
193
239
|
if (media.size && media.size > MAX_FILE_SIZE) {
|
|
194
240
|
return send(`File too large (${Math.round(media.size / 1024 / 1024)}MB). Max: ${MAX_FILE_SIZE / 1024 / 1024}MB`);
|
|
195
241
|
}
|
|
242
|
+
const runContext = admitRunContext(envelope.caption || "", state.currentSession.dir, envelope.messageId, {
|
|
243
|
+
purpose: "foreground",
|
|
244
|
+
attachments: [{ type: "document", mimeType: media.mimeType || null, fileName: media.fileName || null }],
|
|
245
|
+
});
|
|
196
246
|
const savePath = await envelope.adapter.downloadMedia(media);
|
|
197
247
|
const fileName = media.fileName || path.basename(savePath);
|
|
198
248
|
const mime = media.mimeType || "";
|
|
@@ -202,7 +252,11 @@ async function handleDocument(envelope) {
|
|
|
202
252
|
? `Image file saved at ${savePath}\n\nView it, then: ${caption || "Describe this image. If code/UI/error — explain and fix."}`
|
|
203
253
|
: `File received: ${fileName} (${mime})\nSaved at: ${savePath}\n\nRead this file and ${caption || "summarize its contents. If it's code, explain what it does. If it's a document, give key points."}`;
|
|
204
254
|
await send(`File saved: ${fileName}`, { replyTo: envelope.messageId });
|
|
205
|
-
|
|
255
|
+
return runAgent(prompt, runContext.cwd, envelope.messageId, {
|
|
256
|
+
runContext,
|
|
257
|
+
imagePaths: isImage ? [savePath] : [],
|
|
258
|
+
attachments: [{ type: isImage ? "image" : "document", path: savePath, mimeType: mime, fileName }],
|
|
259
|
+
});
|
|
206
260
|
} catch (err) { await send(`Failed: ${err.message}`); }
|
|
207
261
|
}
|
|
208
262
|
|
|
@@ -339,6 +393,9 @@ async function handleText(envelope) {
|
|
|
339
393
|
}
|
|
340
394
|
|
|
341
395
|
const text = envelope.text || "";
|
|
396
|
+
const runContext = admitRunContext(text, state.currentSession.dir, envelope.messageId, { purpose: "foreground" });
|
|
397
|
+
const attachments = [];
|
|
398
|
+
const imagePaths = [];
|
|
342
399
|
const credPatterns = [
|
|
343
400
|
/^(sk-ant-|sk-|glpat-|ghp_|gho_|github_pat_|xoxb-|xoxp-|AKIA|AIza)/,
|
|
344
401
|
/^[A-Za-z0-9_-]{20,}$/,
|
|
@@ -358,6 +415,9 @@ async function handleText(envelope) {
|
|
|
358
415
|
const fileName = reply.document.file_name || reply.document.fileName || "unknown";
|
|
359
416
|
const filePath = path.join(FILES_DIR, fileName);
|
|
360
417
|
if (fs.existsSync(filePath)) {
|
|
418
|
+
const mimeType = reply.document.mime_type || reply.document.mimeType || null;
|
|
419
|
+
attachments.push({ type: mimeType?.startsWith("image/") ? "image" : "document", path: filePath, fileName, mimeType });
|
|
420
|
+
if (mimeType?.startsWith("image/")) imagePaths.push(filePath);
|
|
361
421
|
ctx += (ctx ? "\n" : "") + `[Attached file: ${fileName} at ${filePath}]`;
|
|
362
422
|
} else if (reply.document.file_id) {
|
|
363
423
|
// Attempt to fetch via adapter so this works on every channel.
|
|
@@ -368,6 +428,14 @@ async function handleText(envelope) {
|
|
|
368
428
|
fileName,
|
|
369
429
|
mimeType: reply.document.mime_type,
|
|
370
430
|
});
|
|
431
|
+
const mimeType = reply.document.mime_type || reply.document.mimeType || null;
|
|
432
|
+
attachments.push({
|
|
433
|
+
type: mimeType?.startsWith("image/") ? "image" : "document",
|
|
434
|
+
path: localPath,
|
|
435
|
+
fileName,
|
|
436
|
+
mimeType,
|
|
437
|
+
});
|
|
438
|
+
if (mimeType?.startsWith("image/")) imagePaths.push(localPath);
|
|
371
439
|
ctx += (ctx ? "\n" : "") + `[Attached file: ${fileName} at ${localPath}]`;
|
|
372
440
|
} catch (e) {
|
|
373
441
|
ctx += (ctx ? "\n" : "") + `[Attached file: ${fileName} — could not download]`;
|
|
@@ -383,7 +451,11 @@ async function handleText(envelope) {
|
|
|
383
451
|
}
|
|
384
452
|
}
|
|
385
453
|
|
|
386
|
-
|
|
454
|
+
return runAgent(prompt, runContext.cwd, envelope.messageId, {
|
|
455
|
+
runContext,
|
|
456
|
+
imagePaths,
|
|
457
|
+
attachments,
|
|
458
|
+
});
|
|
387
459
|
}
|
|
388
460
|
|
|
389
|
-
module.exports = { onMessage, onAction };
|
|
461
|
+
module.exports = { onMessage, onAction, deliverRouterError };
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("crypto");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
|
|
6
|
+
function cloneSerializable(value, label = "run context value") {
|
|
7
|
+
if (value === undefined) return undefined;
|
|
8
|
+
try {
|
|
9
|
+
return structuredClone(value);
|
|
10
|
+
} catch (error) {
|
|
11
|
+
const wrapped = new TypeError(`${label} must be serializable`);
|
|
12
|
+
wrapped.code = "INVALID_RUN_CONTEXT";
|
|
13
|
+
wrapped.cause = error;
|
|
14
|
+
throw wrapped;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function deepFreeze(value, seen = new Set()) {
|
|
19
|
+
if (!value || typeof value !== "object" || Object.isFrozen(value) || seen.has(value)) return value;
|
|
20
|
+
seen.add(value);
|
|
21
|
+
for (const child of Object.values(value)) deepFreeze(child, seen);
|
|
22
|
+
return Object.freeze(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectSnapshot(state, cwd, requestedProject) {
|
|
26
|
+
const value = requestedProject !== undefined ? requestedProject : state.currentSession;
|
|
27
|
+
if (value && typeof value === "object") {
|
|
28
|
+
const dir = value.dir || value.path || cwd || null;
|
|
29
|
+
return {
|
|
30
|
+
name: value.name || (dir ? path.basename(dir) : null),
|
|
31
|
+
dir,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (typeof value === "string" && value) {
|
|
35
|
+
return { name: value, dir: cwd || null };
|
|
36
|
+
}
|
|
37
|
+
const dir = cwd || null;
|
|
38
|
+
return { name: dir ? path.basename(dir) : null, dir };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function serializableOrigin(origin, fallback = {}) {
|
|
42
|
+
const source = origin || fallback || {};
|
|
43
|
+
return {
|
|
44
|
+
adapterId: source.adapterId || source.adapter?.id || null,
|
|
45
|
+
transport: source.transport || source.adapter?.type || null,
|
|
46
|
+
channelId: source.channelId != null ? String(source.channelId) : null,
|
|
47
|
+
userId: source.userId != null ? String(source.userId) : null,
|
|
48
|
+
canonicalUserId: source.canonicalUserId || null,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function captureRunContext({
|
|
53
|
+
state,
|
|
54
|
+
store = null,
|
|
55
|
+
prompt = "",
|
|
56
|
+
cwd = null,
|
|
57
|
+
replyToMsgId = null,
|
|
58
|
+
opts = {},
|
|
59
|
+
channelId = null,
|
|
60
|
+
canonicalUserId = null,
|
|
61
|
+
runId = null,
|
|
62
|
+
now = Date.now(),
|
|
63
|
+
} = {}) {
|
|
64
|
+
if (!state || typeof state !== "object") {
|
|
65
|
+
const error = new TypeError("state is required to capture a run context");
|
|
66
|
+
error.code = "INVALID_RUN_CONTEXT";
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
if (typeof prompt !== "string") {
|
|
70
|
+
const error = new TypeError("prompt must be a string");
|
|
71
|
+
error.code = "INVALID_RUN_CONTEXT";
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const provider = String(opts.provider || state.settings?.backend || "").trim().toLowerCase();
|
|
76
|
+
if (provider === "cursor") {
|
|
77
|
+
const error = new Error("Cursor Agent support has been removed; choose Claude Code or OpenAI Codex");
|
|
78
|
+
error.code = "REMOVED_PROVIDER";
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
if (!new Set(["claude", "codex"]).has(provider)) {
|
|
82
|
+
const error = new Error("Choose Claude Code or OpenAI Codex before starting a model turn");
|
|
83
|
+
error.code = "NO_AVAILABLE_PROVIDER";
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
const project = projectSnapshot(state, cwd, opts.project);
|
|
87
|
+
const activeSessionId = state.activeSessions?.[project.name]?.[provider] || null;
|
|
88
|
+
const admittedSessionId = opts.fresh
|
|
89
|
+
? null
|
|
90
|
+
: (opts.resumeSessionId || activeSessionId || null);
|
|
91
|
+
const origin = serializableOrigin(opts.origin, store || {});
|
|
92
|
+
const resolvedChannelId = channelId != null
|
|
93
|
+
? String(channelId)
|
|
94
|
+
: (origin.channelId || (state.channelId != null ? String(state.channelId) : null));
|
|
95
|
+
const providerSettings = cloneSerializable({
|
|
96
|
+
...(state.providerSettings?.[provider] || state.settings || {}),
|
|
97
|
+
...(opts.providerSettings || {}),
|
|
98
|
+
}, "provider settings");
|
|
99
|
+
delete providerSettings.backend;
|
|
100
|
+
|
|
101
|
+
const context = {
|
|
102
|
+
runId: runId || (typeof crypto.randomUUID === "function"
|
|
103
|
+
? crypto.randomUUID()
|
|
104
|
+
: `run-${now}-${Math.random().toString(36).slice(2, 10)}`),
|
|
105
|
+
admittedAt: now,
|
|
106
|
+
canonicalUserId: canonicalUserId || state.userId || origin.canonicalUserId || null,
|
|
107
|
+
project,
|
|
108
|
+
provider,
|
|
109
|
+
providerSettings,
|
|
110
|
+
sessionId: admittedSessionId,
|
|
111
|
+
admittedSessionId,
|
|
112
|
+
previousSessionId: activeSessionId,
|
|
113
|
+
purpose: opts.purpose || (opts.label ? String(opts.label) : "foreground"),
|
|
114
|
+
channelId: resolvedChannelId,
|
|
115
|
+
origin,
|
|
116
|
+
cwd: cwd || project.dir || null,
|
|
117
|
+
replyToMsgId: replyToMsgId == null ? null : String(replyToMsgId),
|
|
118
|
+
userPrompt: prompt,
|
|
119
|
+
fresh: !!opts.fresh,
|
|
120
|
+
continueSession: !!opts.continueSession,
|
|
121
|
+
resumeSessionId: opts.resumeSessionId || null,
|
|
122
|
+
skipAutoCompact: !!opts.skipAutoCompact,
|
|
123
|
+
timeoutMs: Number.isFinite(opts.timeoutMs) ? opts.timeoutMs : null,
|
|
124
|
+
maxTurns: Number.isFinite(opts.maxTurns) ? opts.maxTurns : null,
|
|
125
|
+
imagePaths: cloneSerializable(opts.imagePaths || opts.images || [], "image paths"),
|
|
126
|
+
attachments: cloneSerializable(opts.attachments || [], "attachments"),
|
|
127
|
+
lastInputWasVoice: !!state.lastInputWasVoice,
|
|
128
|
+
voiceStream: !!opts.voiceStream || (!!state.lastInputWasVoice && origin.transport === "voice"),
|
|
129
|
+
readOnly: opts.readOnly === true,
|
|
130
|
+
permissionMode: opts.permissionMode || null,
|
|
131
|
+
transcriptRole: opts.transcriptRole || null,
|
|
132
|
+
label: opts.label || null,
|
|
133
|
+
toolHookSettings: opts.toolHookSettings || null,
|
|
134
|
+
allowReadOnlyWithoutToolHook: opts.allowReadOnlyWithoutToolHook === true,
|
|
135
|
+
requiredDelivery: opts.requiredDelivery !== false,
|
|
136
|
+
attachSession: opts.attachSession !== false,
|
|
137
|
+
occurrenceId: opts.occurrenceId || null,
|
|
138
|
+
scheduledJobId: opts.scheduledJobId || null,
|
|
139
|
+
originRunId: opts.originRunId || null,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
return deepFreeze(context);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function sameCapturedProject(state, runContext) {
|
|
146
|
+
const live = state?.currentSession;
|
|
147
|
+
const captured = runContext?.project;
|
|
148
|
+
if (!live || !captured) return !live && !captured;
|
|
149
|
+
return String(live.name || "") === String(captured.name || "")
|
|
150
|
+
&& String(live.dir || live.path || "") === String(captured.dir || captured.path || "");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function bindRunInputs(runContext, inputs = {}) {
|
|
154
|
+
if (!runContext || typeof runContext !== "object") {
|
|
155
|
+
const error = new TypeError("captured run context is required");
|
|
156
|
+
error.code = "INVALID_RUN_CONTEXT";
|
|
157
|
+
throw error;
|
|
158
|
+
}
|
|
159
|
+
const next = {
|
|
160
|
+
...runContext,
|
|
161
|
+
userPrompt: inputs.userPrompt !== undefined ? String(inputs.userPrompt) : runContext.userPrompt,
|
|
162
|
+
imagePaths: cloneSerializable(
|
|
163
|
+
inputs.imagePaths !== undefined ? inputs.imagePaths : runContext.imagePaths,
|
|
164
|
+
"image paths",
|
|
165
|
+
),
|
|
166
|
+
attachments: cloneSerializable(
|
|
167
|
+
inputs.attachments !== undefined ? inputs.attachments : runContext.attachments,
|
|
168
|
+
"attachments",
|
|
169
|
+
),
|
|
170
|
+
lastInputWasVoice: inputs.lastInputWasVoice !== undefined
|
|
171
|
+
? !!inputs.lastInputWasVoice
|
|
172
|
+
: runContext.lastInputWasVoice,
|
|
173
|
+
voiceStream: inputs.voiceStream !== undefined ? !!inputs.voiceStream : runContext.voiceStream,
|
|
174
|
+
};
|
|
175
|
+
return deepFreeze(next);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
module.exports = {
|
|
179
|
+
captureRunContext,
|
|
180
|
+
bindRunInputs,
|
|
181
|
+
cloneSerializable,
|
|
182
|
+
deepFreeze,
|
|
183
|
+
sameCapturedProject,
|
|
184
|
+
serializableOrigin,
|
|
185
|
+
};
|