talon-agent 3.33.4 → 3.34.1
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/package.json +5 -2
- package/src/app.ts +16 -8
- package/src/backend/claude-sdk/handler.ts +428 -415
- package/src/backend/claude-sdk/stream.ts +76 -53
- package/src/backend/codex/handler/message.ts +275 -366
- package/src/backend/codex/handler/rollout-accounting.ts +137 -0
- package/src/backend/openai-agents/handler/message.ts +282 -356
- package/src/backend/remote-server/chat-turn.ts +66 -122
- package/src/backend/remote-server/index.ts +4 -0
- package/src/backend/remote-server/mcp.ts +73 -9
- package/src/backend/remote-server/model-catalog/presentation.ts +269 -228
- package/src/backend/remote-server/sessions.ts +2 -2
- package/src/backend/remote-server/turn.ts +58 -55
- package/src/backend/shared/cache-telemetry.ts +17 -1
- package/src/backend/shared/handler-to-events.ts +11 -16
- package/src/backend/shared/index.ts +14 -15
- package/src/backend/shared/result-events.ts +30 -0
- package/src/backend/shared/turn-phases.ts +277 -0
- package/src/bootstrap.ts +120 -79
- package/src/cli/setup.ts +375 -349
- package/src/core/background/cron-spec.ts +273 -0
- package/src/core/background/heartbeat/agent.ts +167 -104
- package/src/core/background/triggers/exit.ts +19 -2
- package/src/core/background/triggers/resume.ts +14 -7
- package/src/core/background/triggers/state.ts +9 -0
- package/src/core/engine/backend-controller/pool.ts +2 -0
- package/src/core/engine/backend-controller/state.ts +25 -12
- package/src/core/engine/gateway-actions/cron.ts +28 -292
- package/src/core/engine/gateway-routes.ts +239 -0
- package/src/core/engine/gateway.ts +66 -238
- package/src/core/mcp-hub/child-transport.ts +215 -0
- package/src/core/mcp-hub/children.ts +72 -13
- package/src/core/mcp-hub/index.ts +32 -13
- package/src/core/models/active-model.ts +29 -6
- package/src/core/vfs/mounts/files.ts +128 -115
- package/src/core/weaver/shuttle.ts +33 -2
- package/src/core/weaver/weaver.ts +37 -6
- package/src/frontend/discord/callbacks/components/agent-buttons.ts +82 -0
- package/src/frontend/discord/callbacks/components/backend-select.ts +149 -0
- package/src/frontend/discord/callbacks/components/effort.ts +72 -0
- package/src/frontend/discord/callbacks/components/index.ts +120 -0
- package/src/frontend/discord/callbacks/components/metrics.ts +24 -0
- package/src/frontend/discord/callbacks/components/model-nav.ts +93 -0
- package/src/frontend/discord/callbacks/components/model-select.ts +118 -0
- package/src/frontend/discord/callbacks/components/model.ts +33 -0
- package/src/frontend/discord/callbacks/components/pulse.ts +93 -0
- package/src/frontend/discord/callbacks/components/settings.ts +243 -0
- package/src/frontend/discord/callbacks/components/types.ts +34 -0
- package/src/frontend/discord/callbacks/index.ts +4 -4
- package/src/frontend/discord/connection.ts +36 -0
- package/src/frontend/discord/diagnostics.ts +62 -0
- package/src/frontend/discord/guild-policy.ts +89 -0
- package/src/frontend/discord/index.ts +44 -305
- package/src/frontend/discord/outbound.ts +61 -0
- package/src/frontend/discord/ready.ts +73 -0
- package/src/frontend/discord/runtime.ts +55 -0
- package/src/frontend/native/chat-lifecycle.ts +39 -0
- package/src/frontend/native/chat-wire.ts +69 -0
- package/src/frontend/native/context.ts +106 -0
- package/src/frontend/native/control.ts +78 -0
- package/src/frontend/native/emit.ts +167 -0
- package/src/frontend/native/empty-chat-sweep.ts +50 -0
- package/src/frontend/native/handlers.ts +121 -0
- package/src/frontend/native/history.ts +101 -0
- package/src/frontend/native/index.ts +90 -1293
- package/src/frontend/native/media.ts +43 -0
- package/src/frontend/native/models.ts +221 -0
- package/src/frontend/native/queue.ts +47 -0
- package/src/frontend/native/reset.ts +50 -0
- package/src/frontend/native/routes/chats.ts +115 -0
- package/src/frontend/native/routes/daemon.ts +70 -0
- package/src/frontend/native/routes/host.ts +151 -0
- package/src/frontend/native/routes/index.ts +22 -0
- package/src/frontend/native/routes/mesh.ts +72 -0
- package/src/frontend/native/routes/models.ts +54 -0
- package/src/frontend/native/routes/params.ts +29 -0
- package/src/frontend/native/routes/pre-auth.ts +94 -0
- package/src/frontend/native/routes/table.ts +92 -0
- package/src/frontend/native/runtime.ts +109 -0
- package/src/frontend/native/server.ts +30 -555
- package/src/frontend/native/status.ts +26 -0
- package/src/frontend/native/tool-result.ts +48 -0
- package/src/frontend/native/turn.ts +341 -0
- package/src/frontend/telegram/admin.ts +75 -52
- package/src/frontend/whatsapp/access.ts +67 -0
- package/src/frontend/whatsapp/connection.ts +280 -0
- package/src/frontend/whatsapp/inbound.ts +327 -0
- package/src/frontend/whatsapp/index.ts +28 -599
- package/src/frontend/whatsapp/runtime.ts +74 -0
- package/src/storage/metrics.ts +18 -0
- package/src/storage/session-record.ts +29 -0
- package/src/storage/sessions.ts +24 -0
- package/src/storage/trigger-store.ts +8 -0
- package/src/util/boot-timer.ts +31 -0
- package/src/util/concurrency.ts +28 -0
- package/src/util/watchdog.ts +32 -7
- package/src/frontend/discord/callbacks/components.ts +0 -793
- package/src/frontend/discord/callbacks/shared.ts +0 -22
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Media handling — the id → path registry the `/media` route serves from,
|
|
3
|
+
* and the uploads directory client attachments land in.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
7
|
+
import { basename, join } from "node:path";
|
|
8
|
+
import { dirs } from "../../util/paths.js";
|
|
9
|
+
import type { NativeRuntime } from "./runtime.js";
|
|
10
|
+
|
|
11
|
+
/** Register a file for serving and return its short media id. */
|
|
12
|
+
export function registerMedia(
|
|
13
|
+
runtime: NativeRuntime,
|
|
14
|
+
filePath: string,
|
|
15
|
+
): string {
|
|
16
|
+
const id = `m${runtime.nextId().toString(36)}`;
|
|
17
|
+
runtime.media.set(id, filePath);
|
|
18
|
+
return id;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** The relative bridge path a client renders a registered media id from. */
|
|
22
|
+
export function mediaUrl(mediaId: string): string {
|
|
23
|
+
return `/media?id=${encodeURIComponent(mediaId)}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Persist an uploaded attachment to the workspace uploads dir under a safe,
|
|
28
|
+
* unique name, and return its absolute path (handed to the model to read).
|
|
29
|
+
*/
|
|
30
|
+
export async function saveUpload(
|
|
31
|
+
runtime: NativeRuntime,
|
|
32
|
+
filename: string,
|
|
33
|
+
bytes: Buffer,
|
|
34
|
+
): Promise<string> {
|
|
35
|
+
await mkdir(dirs.uploads, { recursive: true });
|
|
36
|
+
const safe = basename(filename).replace(/[^\w.-]+/g, "_") || "upload";
|
|
37
|
+
const dest = join(
|
|
38
|
+
dirs.uploads,
|
|
39
|
+
`${Date.now()}-${runtime.nextId().toString(36)}-${safe}`,
|
|
40
|
+
);
|
|
41
|
+
await writeFile(dest, bytes);
|
|
42
|
+
return dest;
|
|
43
|
+
}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-chat model, backend and effort selection — the pickers behind the
|
|
3
|
+
* companion app's model sheet. The persisted per-chat setting is always
|
|
4
|
+
* consulted before the live pool binding (see `toClientChat` for why).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
getChatSettings,
|
|
9
|
+
setChatEffort,
|
|
10
|
+
setChatModelForBackend,
|
|
11
|
+
getChatModelForBackend,
|
|
12
|
+
setChatBackend,
|
|
13
|
+
EFFORT_LEVELS,
|
|
14
|
+
type EffortLevel,
|
|
15
|
+
} from "../../storage/chat-settings.js";
|
|
16
|
+
import {
|
|
17
|
+
getBackendForChat,
|
|
18
|
+
getBackendIdForChat,
|
|
19
|
+
getPooledBackend,
|
|
20
|
+
acquireBackendInstance,
|
|
21
|
+
listAvailableBackends,
|
|
22
|
+
rebindChat,
|
|
23
|
+
} from "../../core/engine/backend-controller/index.js";
|
|
24
|
+
import { getActiveReasoningLevels } from "../shared/reasoning-levels.js";
|
|
25
|
+
import { broadcastChatUpdated } from "./chat-wire.js";
|
|
26
|
+
import { emitSystem } from "./emit.js";
|
|
27
|
+
import type { BackendOption, ModelOption } from "./protocol.js";
|
|
28
|
+
import { wipeChatConversation } from "./reset.js";
|
|
29
|
+
import type { NativeRuntime } from "./runtime.js";
|
|
30
|
+
import { broadcastStatus } from "./status.js";
|
|
31
|
+
|
|
32
|
+
export async function listModels(
|
|
33
|
+
runtime: NativeRuntime,
|
|
34
|
+
chatId?: string,
|
|
35
|
+
): Promise<{ active: string; models: ModelOption[] }> {
|
|
36
|
+
const { config } = runtime;
|
|
37
|
+
// Resolve the chat's *own* backend so the model list tracks whatever
|
|
38
|
+
// backend the chat is currently bound to (fixes the list staying on the
|
|
39
|
+
// previous backend's models after a switch). Fall back to the global
|
|
40
|
+
// default backend when there's no chat / the pool isn't ready yet.
|
|
41
|
+
let backendId: string = config.backend;
|
|
42
|
+
let active = config.model;
|
|
43
|
+
if (chatId) {
|
|
44
|
+
try {
|
|
45
|
+
// Persisted setting first — see toClientChat.
|
|
46
|
+
backendId =
|
|
47
|
+
getChatSettings(chatId).backend ?? getBackendIdForChat(chatId);
|
|
48
|
+
active = getChatModelForBackend(chatId, backendId) ?? config.model;
|
|
49
|
+
} catch {
|
|
50
|
+
/* pool not ready — keep global defaults */
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Pull the models dynamically from the gateway for that backend. Prefer
|
|
55
|
+
// an already-pooled instance of the *resolved* backend (the persisted
|
|
56
|
+
// choice — the chat's live binding can lag it after a failed boot-time
|
|
57
|
+
// rebind, and listing the live binding's catalog here showed the wrong
|
|
58
|
+
// backend's models); otherwise boot the backend transiently to read its
|
|
59
|
+
// catalog, then release it so we never leak an instance — mirroring the
|
|
60
|
+
// shared `list_models` action.
|
|
61
|
+
let instance:
|
|
62
|
+
| Awaited<ReturnType<typeof acquireBackendInstance>>["backend"]
|
|
63
|
+
| null
|
|
64
|
+
| undefined = getPooledBackend(backendId);
|
|
65
|
+
let release: (() => Promise<void>) | null = null;
|
|
66
|
+
if (!instance) {
|
|
67
|
+
try {
|
|
68
|
+
const acquired = await acquireBackendInstance(backendId);
|
|
69
|
+
instance = acquired.backend;
|
|
70
|
+
release = acquired.release;
|
|
71
|
+
} catch {
|
|
72
|
+
return { active, models: [] };
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const catalog = instance.models;
|
|
78
|
+
if (!catalog?.listModels) return { active, models: [] };
|
|
79
|
+
const { models } = await catalog.listModels("all");
|
|
80
|
+
const options: ModelOption[] = models
|
|
81
|
+
.filter((m) => m.selectable)
|
|
82
|
+
.map((m) => ({
|
|
83
|
+
id: m.id,
|
|
84
|
+
displayName: m.displayName,
|
|
85
|
+
provider: m.provider,
|
|
86
|
+
reasoning:
|
|
87
|
+
Boolean(m.supportedReasoningLevels?.length) || Boolean(m.reasoning),
|
|
88
|
+
}));
|
|
89
|
+
return { active, models: options };
|
|
90
|
+
} catch {
|
|
91
|
+
return { active, models: [] };
|
|
92
|
+
} finally {
|
|
93
|
+
if (release) await release();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function setModel(
|
|
98
|
+
runtime: NativeRuntime,
|
|
99
|
+
chatId: string,
|
|
100
|
+
model: string,
|
|
101
|
+
): void {
|
|
102
|
+
const entry = runtime.chats.get(chatId);
|
|
103
|
+
if (!entry) return;
|
|
104
|
+
// Persisted setting first — see toClientChat. Writing the pick under the
|
|
105
|
+
// live binding's key would attach it to the wrong backend whenever the
|
|
106
|
+
// boot-time rebind to the persisted backend is still pending.
|
|
107
|
+
const backendId =
|
|
108
|
+
getChatSettings(chatId).backend ?? getBackendIdForChat(chatId);
|
|
109
|
+
setChatModelForBackend(chatId, backendId, model.trim() || undefined);
|
|
110
|
+
broadcastChatUpdated(runtime, entry);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function listBackends(
|
|
114
|
+
runtime: NativeRuntime,
|
|
115
|
+
chatId: string,
|
|
116
|
+
): { active: string; backends: BackendOption[] } {
|
|
117
|
+
const { config } = runtime;
|
|
118
|
+
const backends = listAvailableBackends(config);
|
|
119
|
+
let active: string = config.backend;
|
|
120
|
+
try {
|
|
121
|
+
if (chatId) {
|
|
122
|
+
// Persisted setting first — see toClientChat.
|
|
123
|
+
active = getChatSettings(chatId).backend ?? getBackendIdForChat(chatId);
|
|
124
|
+
}
|
|
125
|
+
} catch {
|
|
126
|
+
/* pool not ready — report the global default backend */
|
|
127
|
+
}
|
|
128
|
+
return { active, backends };
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Switch a chat to another backend. Mirrors the Telegram `/model` backend
|
|
133
|
+
* submenu: verify the target is enabled, rebind the chat's pool holder, pin
|
|
134
|
+
* the override, and drop the previous backend's per-chat session state
|
|
135
|
+
* (sessions aren't portable across backends). Per-backend model picks are
|
|
136
|
+
* kept, so switching back restores the prior model automatically.
|
|
137
|
+
*/
|
|
138
|
+
export async function setBackend(
|
|
139
|
+
runtime: NativeRuntime,
|
|
140
|
+
chatId: string,
|
|
141
|
+
backend: string,
|
|
142
|
+
): Promise<{ ok: boolean; error?: string }> {
|
|
143
|
+
const { config } = runtime;
|
|
144
|
+
const entry = runtime.chats.get(chatId);
|
|
145
|
+
if (!entry) return { ok: false, error: "No such chat" };
|
|
146
|
+
const target = backend.trim();
|
|
147
|
+
const available = listAvailableBackends(config);
|
|
148
|
+
if (!available.some((b) => b.id === target)) {
|
|
149
|
+
return { ok: false, error: "Backend not available" };
|
|
150
|
+
}
|
|
151
|
+
const persisted = getChatSettings(chatId).backend;
|
|
152
|
+
if (getBackendIdForChat(chatId) === target) {
|
|
153
|
+
// Already live on the target — just make sure the choice is persisted.
|
|
154
|
+
if (persisted !== target && target !== config.backend) {
|
|
155
|
+
setChatBackend(chatId, target);
|
|
156
|
+
broadcastChatUpdated(runtime, entry);
|
|
157
|
+
}
|
|
158
|
+
return { ok: true };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const result = await rebindChat(chatId, target, config);
|
|
162
|
+
if (!result.ok) {
|
|
163
|
+
return { ok: false, error: result.error ?? "Rebind failed" };
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Re-selecting the backend this chat is already persisted to is a
|
|
167
|
+
// RE-ATTACH (e.g. the boot-time rebind failed transiently and the user —
|
|
168
|
+
// or a client retry — picks it again). The conversation belongs to that
|
|
169
|
+
// backend; resetting the session and wiping history here destroyed real
|
|
170
|
+
// conversations and surfaced as a phantom "backend reset" in clients.
|
|
171
|
+
if (persisted === target) {
|
|
172
|
+
broadcastChatUpdated(runtime, entry);
|
|
173
|
+
broadcastStatus(runtime);
|
|
174
|
+
return { ok: true };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
setChatBackend(chatId, target);
|
|
178
|
+
wipeChatConversation(runtime, chatId);
|
|
179
|
+
emitSystem(
|
|
180
|
+
runtime,
|
|
181
|
+
entry,
|
|
182
|
+
`Switched to ${target} — starting a fresh conversation.`,
|
|
183
|
+
);
|
|
184
|
+
broadcastChatUpdated(runtime, entry);
|
|
185
|
+
broadcastStatus(runtime);
|
|
186
|
+
return { ok: true };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function setEffort(
|
|
190
|
+
runtime: NativeRuntime,
|
|
191
|
+
chatId: string,
|
|
192
|
+
effort: string,
|
|
193
|
+
): void {
|
|
194
|
+
const entry = runtime.chats.get(chatId);
|
|
195
|
+
if (!entry) return;
|
|
196
|
+
const level = EFFORT_LEVELS.includes(effort as EffortLevel)
|
|
197
|
+
? (effort as EffortLevel)
|
|
198
|
+
: undefined;
|
|
199
|
+
setChatEffort(chatId, level);
|
|
200
|
+
broadcastChatUpdated(runtime, entry);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export async function effortLevels(
|
|
204
|
+
runtime: NativeRuntime,
|
|
205
|
+
chatId: string,
|
|
206
|
+
): Promise<{ active: string; levels: string[] }> {
|
|
207
|
+
try {
|
|
208
|
+
const backend = getBackendForChat(chatId);
|
|
209
|
+
const backendId = getBackendIdForChat(chatId);
|
|
210
|
+
const { levels } = await getActiveReasoningLevels({
|
|
211
|
+
chatId,
|
|
212
|
+
backend,
|
|
213
|
+
backendId,
|
|
214
|
+
config: runtime.config,
|
|
215
|
+
});
|
|
216
|
+
const active = getChatSettings(chatId).effort ?? "adaptive";
|
|
217
|
+
return { active, levels };
|
|
218
|
+
} catch {
|
|
219
|
+
return { active: "adaptive", levels: [] };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queued follow-up bookkeeping — the one message a chat holds while a turn
|
|
3
|
+
* is running. Every mutation is synced to clients as a `chat_updated`.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { broadcastChatUpdated } from "./chat-wire.js";
|
|
7
|
+
import type { ChatEntry } from "./chats.js";
|
|
8
|
+
import type { NativeRuntime, QueuedEntry } from "./runtime.js";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Set (or replace) a chat's queued follow-up and sync it to every client via
|
|
12
|
+
* chat_updated. Empty text with no attachment clears the queue.
|
|
13
|
+
*/
|
|
14
|
+
export function setQueued(
|
|
15
|
+
runtime: NativeRuntime,
|
|
16
|
+
chatId: string,
|
|
17
|
+
next: QueuedEntry,
|
|
18
|
+
): void {
|
|
19
|
+
const entry = runtime.chats.get(chatId);
|
|
20
|
+
if (!entry) return;
|
|
21
|
+
const text = next.text.trim();
|
|
22
|
+
if (!text && !next.attachmentPath) {
|
|
23
|
+
if (!runtime.queuedByChat.delete(chatId)) return;
|
|
24
|
+
} else {
|
|
25
|
+
runtime.queuedByChat.set(chatId, {
|
|
26
|
+
text,
|
|
27
|
+
imagePath: next.imagePath,
|
|
28
|
+
attachmentPath: next.attachmentPath,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
broadcastChatUpdated(runtime, entry);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Remove and return a chat's queued follow-up (syncing the now-empty queue
|
|
36
|
+
* to clients), or undefined when nothing was queued.
|
|
37
|
+
*/
|
|
38
|
+
export function takeQueued(
|
|
39
|
+
runtime: NativeRuntime,
|
|
40
|
+
entry: ChatEntry,
|
|
41
|
+
): QueuedEntry | undefined {
|
|
42
|
+
const queued = runtime.queuedByChat.get(entry.id);
|
|
43
|
+
if (!queued) return undefined;
|
|
44
|
+
runtime.queuedByChat.delete(entry.id);
|
|
45
|
+
broadcastChatUpdated(runtime, entry);
|
|
46
|
+
return queued;
|
|
47
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chat reset — drop a chat's conversation (session, history, turn meta,
|
|
3
|
+
* cached readouts, pulse checkpoint) while keeping the chat itself.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { resetSession } from "../../storage/sessions.js";
|
|
7
|
+
import { clearHistory } from "../../storage/history.js";
|
|
8
|
+
import { resetPulseCheckpoint } from "../../core/background/pulse.js";
|
|
9
|
+
import { getBackendForChat } from "../../core/engine/backend-controller/index.js";
|
|
10
|
+
import { broadcastChatUpdated } from "./chat-wire.js";
|
|
11
|
+
import { emitSystem } from "./emit.js";
|
|
12
|
+
import type { NativeRuntime } from "./runtime.js";
|
|
13
|
+
import { clearTurnMeta } from "./turn-meta.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Forget everything the conversation accumulated. Shared by the explicit
|
|
17
|
+
* reset and a backend switch (sessions aren't portable across backends).
|
|
18
|
+
*/
|
|
19
|
+
export function wipeChatConversation(
|
|
20
|
+
runtime: NativeRuntime,
|
|
21
|
+
chatId: string,
|
|
22
|
+
): void {
|
|
23
|
+
resetSession(chatId);
|
|
24
|
+
clearHistory(chatId);
|
|
25
|
+
clearTurnMeta(chatId);
|
|
26
|
+
runtime.contextByChat.delete(chatId);
|
|
27
|
+
runtime.queuedByChat.delete(chatId);
|
|
28
|
+
resetPulseCheckpoint(chatId);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function resetChat(runtime: NativeRuntime, chatId: string): boolean {
|
|
32
|
+
const entry = runtime.chats.get(chatId);
|
|
33
|
+
if (!entry) return false;
|
|
34
|
+
// Full reset, matching /reset on the other frontends: session,
|
|
35
|
+
// history (the app re-fetches its transcript from us), pulse
|
|
36
|
+
// checkpoint, and any in-process backend memory. Warm the fresh
|
|
37
|
+
// session in the background — the bridge handler is sync.
|
|
38
|
+
wipeChatConversation(runtime, chatId);
|
|
39
|
+
let backend = null;
|
|
40
|
+
try {
|
|
41
|
+
backend = getBackendForChat(chatId);
|
|
42
|
+
} catch {
|
|
43
|
+
// No pool binding — nothing to wipe or warm.
|
|
44
|
+
}
|
|
45
|
+
backend?.sessions?.resetChat?.(chatId);
|
|
46
|
+
void backend?.sessions?.warmSession?.(chatId)?.catch(() => {});
|
|
47
|
+
emitSystem(runtime, entry, "Session reset — starting a fresh conversation.");
|
|
48
|
+
broadcastChatUpdated(runtime, entry);
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import type { RouteHost } from "./host.js";
|
|
2
|
+
import type { BridgeRoutes } from "./table.js";
|
|
3
|
+
import { asPositiveInt, asString, deviceIdParam } from "./params.js";
|
|
4
|
+
|
|
5
|
+
const MAX_UPLOAD_BYTES = 25 * 1024 * 1024;
|
|
6
|
+
|
|
7
|
+
export function chatRoutes(
|
|
8
|
+
host: RouteHost,
|
|
9
|
+
): Pick<
|
|
10
|
+
BridgeRoutes,
|
|
11
|
+
| "GET /events"
|
|
12
|
+
| "GET /chats"
|
|
13
|
+
| "POST /chats"
|
|
14
|
+
| "POST /chats/rename"
|
|
15
|
+
| "POST /chats/delete"
|
|
16
|
+
| "POST /chats/reset"
|
|
17
|
+
| "POST /chats/interrupt"
|
|
18
|
+
| "POST /chats/pulse"
|
|
19
|
+
| "POST /queue"
|
|
20
|
+
| "GET /history"
|
|
21
|
+
| "GET /search"
|
|
22
|
+
| "POST /send"
|
|
23
|
+
| "POST /upload"
|
|
24
|
+
| "GET /media"
|
|
25
|
+
> {
|
|
26
|
+
const { json, readJson, handlers: h } = host;
|
|
27
|
+
return {
|
|
28
|
+
// ── Chats ──────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
// A mesh client names itself here so device-addressed events reach
|
|
31
|
+
// it alone (see sendToDevice); UI clients simply omit it.
|
|
32
|
+
"GET /events": ({ res, url }) => host.openStream(res, deviceIdParam(url)),
|
|
33
|
+
"GET /chats": ({ res }) => json(res, 200, { chats: h.listChats() }),
|
|
34
|
+
"POST /chats": async ({ req, res }) => {
|
|
35
|
+
const body = await readJson(req);
|
|
36
|
+
json(res, 200, { chat: h.createChat(asString(body.title)) });
|
|
37
|
+
},
|
|
38
|
+
"POST /chats/rename": async ({ req, res }) => {
|
|
39
|
+
const body = await readJson(req);
|
|
40
|
+
const chat = h.renameChat(
|
|
41
|
+
asString(body.chatId) ?? "",
|
|
42
|
+
asString(body.title) ?? "",
|
|
43
|
+
);
|
|
44
|
+
return chat
|
|
45
|
+
? json(res, 200, { chat })
|
|
46
|
+
: json(res, 404, { ok: false, error: "No such chat" });
|
|
47
|
+
},
|
|
48
|
+
"POST /chats/delete": async ({ req, res }) => {
|
|
49
|
+
const body = await readJson(req);
|
|
50
|
+
json(res, 200, { ok: h.deleteChat(asString(body.chatId) ?? "") });
|
|
51
|
+
},
|
|
52
|
+
"POST /chats/reset": async ({ req, res }) => {
|
|
53
|
+
const body = await readJson(req);
|
|
54
|
+
json(res, 200, { ok: h.resetChat(asString(body.chatId) ?? "") });
|
|
55
|
+
},
|
|
56
|
+
"POST /chats/interrupt": async ({ req, res }) => {
|
|
57
|
+
const body = await readJson(req);
|
|
58
|
+
const ok = await h.interruptTurn(asString(body.chatId) ?? "");
|
|
59
|
+
json(res, 200, { ok });
|
|
60
|
+
},
|
|
61
|
+
"POST /chats/pulse": async ({ req, res }) => {
|
|
62
|
+
const body = await readJson(req);
|
|
63
|
+
h.setPulse(asString(body.chatId) ?? "", body.on === true);
|
|
64
|
+
json(res, 200, { ok: true });
|
|
65
|
+
},
|
|
66
|
+
"POST /queue": async ({ req, res }) => {
|
|
67
|
+
const body = await readJson(req);
|
|
68
|
+
h.queueMessage(asString(body.chatId) ?? "", asString(body.text) ?? "");
|
|
69
|
+
json(res, 200, { ok: true });
|
|
70
|
+
},
|
|
71
|
+
"GET /history": ({ res, url }) => {
|
|
72
|
+
const id = url.searchParams.get("chatId") ?? "";
|
|
73
|
+
const before = asPositiveInt(url.searchParams.get("before"));
|
|
74
|
+
const limit = asPositiveInt(url.searchParams.get("limit"));
|
|
75
|
+
json(res, 200, {
|
|
76
|
+
chatId: id,
|
|
77
|
+
messages: h.history(id, { before, limit }),
|
|
78
|
+
});
|
|
79
|
+
},
|
|
80
|
+
"GET /search": ({ res, url }) => {
|
|
81
|
+
const q = (url.searchParams.get("q") ?? "").trim();
|
|
82
|
+
if (!q) return json(res, 400, { ok: false, error: "q required" });
|
|
83
|
+
const chatId = url.searchParams.get("chatId") ?? undefined;
|
|
84
|
+
json(res, 200, { results: h.search(q, chatId) });
|
|
85
|
+
},
|
|
86
|
+
"POST /send": async ({ req, res }) => {
|
|
87
|
+
const body = await readJson(req);
|
|
88
|
+
const id = asString(body.chatId) ?? "";
|
|
89
|
+
const text = asString(body.text) ?? "";
|
|
90
|
+
const imagePath = asString(body.imagePath);
|
|
91
|
+
const attachmentPath = asString(body.attachmentPath);
|
|
92
|
+
// Text may be empty when an image is attached; require one or the
|
|
93
|
+
// other.
|
|
94
|
+
if (!id || (!text.trim() && !attachmentPath))
|
|
95
|
+
return json(res, 400, {
|
|
96
|
+
ok: false,
|
|
97
|
+
error: "chatId and text (or an attachment) required",
|
|
98
|
+
});
|
|
99
|
+
h.send(id, text, { imagePath, attachmentPath });
|
|
100
|
+
json(res, 202, { ok: true });
|
|
101
|
+
},
|
|
102
|
+
"POST /upload": async ({ req, res, url }) => {
|
|
103
|
+
const filename = url.searchParams.get("filename") ?? "upload";
|
|
104
|
+
const contentType =
|
|
105
|
+
req.headers["content-type"] ?? "application/octet-stream";
|
|
106
|
+
const bytes = await host.readRaw(req, MAX_UPLOAD_BYTES);
|
|
107
|
+
if (!bytes.length)
|
|
108
|
+
return json(res, 400, { ok: false, error: "Empty upload" });
|
|
109
|
+
const result = await h.upload(filename, contentType, bytes);
|
|
110
|
+
json(res, 200, { ok: true, ...result });
|
|
111
|
+
},
|
|
112
|
+
"GET /media": ({ res, url }) =>
|
|
113
|
+
host.serveMedia(res, url.searchParams.get("id") ?? ""),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { RouteHost } from "./host.js";
|
|
2
|
+
import type { BridgeRoutes } from "./table.js";
|
|
3
|
+
import { isLogLevel } from "../protocol.js";
|
|
4
|
+
import { asPositiveInt, asString } from "./params.js";
|
|
5
|
+
|
|
6
|
+
export function daemonRoutes(
|
|
7
|
+
host: RouteHost,
|
|
8
|
+
): Pick<
|
|
9
|
+
BridgeRoutes,
|
|
10
|
+
| "GET /logs"
|
|
11
|
+
| "GET /plugins"
|
|
12
|
+
| "POST /plugins/toggle"
|
|
13
|
+
| "GET /skills"
|
|
14
|
+
| "POST /skills/toggle"
|
|
15
|
+
| "GET /config"
|
|
16
|
+
| "POST /config"
|
|
17
|
+
| "POST /control"
|
|
18
|
+
> {
|
|
19
|
+
const { json, readJson, handlers: h } = host;
|
|
20
|
+
return {
|
|
21
|
+
// ── Daemon ─────────────────────────────────────────────────────────
|
|
22
|
+
|
|
23
|
+
"GET /logs": ({ res, url }) => {
|
|
24
|
+
const lines = Math.min(
|
|
25
|
+
asPositiveInt(url.searchParams.get("lines")) ?? 200,
|
|
26
|
+
1000,
|
|
27
|
+
);
|
|
28
|
+
const level = url.searchParams.get("level") ?? "";
|
|
29
|
+
const component = url.searchParams.get("component") ?? undefined;
|
|
30
|
+
json(res, 200, {
|
|
31
|
+
entries: h.logs({
|
|
32
|
+
lines,
|
|
33
|
+
minLevel: isLogLevel(level) ? level : undefined,
|
|
34
|
+
component,
|
|
35
|
+
}),
|
|
36
|
+
});
|
|
37
|
+
},
|
|
38
|
+
"GET /plugins": ({ res }) => json(res, 200, { plugins: h.listPlugins() }),
|
|
39
|
+
"POST /plugins/toggle": async ({ req, res }) => {
|
|
40
|
+
const body = await readJson(req);
|
|
41
|
+
// Always 200: ok/error is an application result the client renders
|
|
42
|
+
// (mirrors /backend).
|
|
43
|
+
const result = await h.setPluginEnabled(
|
|
44
|
+
asString(body.name) ?? "",
|
|
45
|
+
body.enabled === true,
|
|
46
|
+
);
|
|
47
|
+
json(res, 200, result);
|
|
48
|
+
},
|
|
49
|
+
"GET /skills": ({ res }) => json(res, 200, { skills: h.listSkills() }),
|
|
50
|
+
"POST /skills/toggle": async ({ req, res }) => {
|
|
51
|
+
const body = await readJson(req);
|
|
52
|
+
json(
|
|
53
|
+
res,
|
|
54
|
+
200,
|
|
55
|
+
h.setSkillEnabled(asString(body.name) ?? "", body.enabled === true),
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
"GET /config": ({ res }) => json(res, 200, h.getConfig()),
|
|
59
|
+
"POST /config": async ({ req, res }) => {
|
|
60
|
+
const body = await readJson(req);
|
|
61
|
+
json(res, 200, h.setConfig(body));
|
|
62
|
+
},
|
|
63
|
+
"POST /control": async ({ req, res }) => {
|
|
64
|
+
const body = await readJson(req);
|
|
65
|
+
// Always 200: ok/message is an application result the client
|
|
66
|
+
// renders (mirrors /backend).
|
|
67
|
+
json(res, 200, await h.control(asString(body.action) ?? ""));
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
}
|