@schlessera/brain-ui-server 0.15.0 → 0.17.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/README.md +4 -0
- package/dist/agent/backend.d.ts +31 -10
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +92 -28
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts +9 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +80 -18
- package/dist/app.js.map +1 -1
- package/dist/config/env-core.d.ts +73 -0
- package/dist/config/env-core.d.ts.map +1 -0
- package/dist/config/env-core.js +62 -0
- package/dist/config/env-core.js.map +1 -0
- package/dist/config/env.d.ts +27 -5
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +90 -10
- package/dist/config/env.js.map +1 -1
- package/dist/cron/scheduler.d.ts +20 -0
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +43 -8
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/client.d.ts +10 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +5 -14
- package/dist/db/client.js.map +1 -1
- package/dist/db/settings.d.ts +3 -2
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +8 -4
- package/dist/db/settings.js.map +1 -1
- package/dist/index.d.ts +6 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -2
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +8 -2
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +74 -10
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +10 -0
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +19 -3
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/observability/index.d.ts +73 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +74 -0
- package/dist/observability/index.js.map +1 -0
- package/dist/observability/loggers.d.ts +62 -0
- package/dist/observability/loggers.d.ts.map +1 -0
- package/dist/observability/loggers.js +109 -0
- package/dist/observability/loggers.js.map +1 -0
- package/dist/observability/meter.d.ts +39 -0
- package/dist/observability/meter.d.ts.map +1 -0
- package/dist/observability/meter.js +95 -0
- package/dist/observability/meter.js.map +1 -0
- package/dist/observability/types.d.ts +67 -0
- package/dist/observability/types.d.ts.map +1 -0
- package/dist/observability/types.js +44 -0
- package/dist/observability/types.js.map +1 -0
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +9 -1
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts +2 -0
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +7 -7
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/graph.d.ts +2 -0
- package/dist/routes/graph.d.ts.map +1 -1
- package/dist/routes/graph.js +7 -7
- package/dist/routes/graph.js.map +1 -1
- package/dist/routes/health.d.ts +7 -14
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +25 -6
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/models.d.ts +3 -0
- package/dist/routes/models.d.ts.map +1 -1
- package/dist/routes/models.js +5 -1
- package/dist/routes/models.js.map +1 -1
- package/dist/routes/render.d.ts +2 -1
- package/dist/routes/render.d.ts.map +1 -1
- package/dist/routes/render.js +2 -2
- package/dist/routes/render.js.map +1 -1
- package/dist/routes/share.d.ts +3 -0
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +5 -5
- package/dist/routes/share.js.map +1 -1
- package/dist/share/staging.d.ts +3 -2
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +12 -4
- package/dist/share/staging.js.map +1 -1
- package/dist/voice/keyterm-builder.d.ts +4 -1
- package/dist/voice/keyterm-builder.d.ts.map +1 -1
- package/dist/voice/keyterm-builder.js +15 -8
- package/dist/voice/keyterm-builder.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +23 -3
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +10 -3
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +16 -4
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts +15 -0
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +66 -10
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts +22 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +36 -16
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +73 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +127 -1
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/rate-limit.d.ts +58 -0
- package/dist/ws/rate-limit.d.ts.map +1 -0
- package/dist/ws/rate-limit.js +62 -0
- package/dist/ws/rate-limit.js.map +1 -0
- package/dist/ws/run-session.d.ts +3 -0
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +49 -9
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/session-catalog.d.ts +2 -1
- package/dist/ws/session-catalog.d.ts.map +1 -1
- package/dist/ws/session-catalog.js +20 -3
- package/dist/ws/session-catalog.js.map +1 -1
- package/dist/ws/turns.d.ts +21 -0
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +11 -1
- package/dist/ws/turns.js.map +1 -1
- package/package.json +9 -6
- package/src/agent/backend.ts +131 -35
- package/src/app.ts +97 -18
- package/src/config/env-core.ts +93 -0
- package/src/config/env.ts +116 -15
- package/src/cron/scheduler.ts +61 -16
- package/src/db/client.ts +16 -5
- package/src/db/settings.ts +9 -4
- package/src/index.ts +43 -2
- package/src/middleware/auth.ts +80 -15
- package/src/middleware/passkeys.ts +29 -5
- package/src/observability/index.ts +150 -0
- package/src/observability/loggers.ts +174 -0
- package/src/observability/meter.ts +190 -0
- package/src/observability/types.ts +101 -0
- package/src/routes/brain.ts +10 -1
- package/src/routes/files.ts +9 -8
- package/src/routes/graph.ts +9 -8
- package/src/routes/health.ts +29 -6
- package/src/routes/models.ts +8 -5
- package/src/routes/render.ts +3 -2
- package/src/routes/share.ts +17 -10
- package/src/share/staging.ts +18 -4
- package/src/voice/keyterm-builder.ts +19 -10
- package/src/ws/bridge.ts +22 -6
- package/src/ws/clients.ts +24 -6
- package/src/ws/connection.ts +73 -13
- package/src/ws/dispatch.ts +47 -16
- package/src/ws/host.ts +164 -1
- package/src/ws/rate-limit.ts +86 -0
- package/src/ws/run-session.ts +57 -14
- package/src/ws/session-catalog.ts +21 -3
- package/src/ws/turns.ts +27 -3
package/src/ws/run-session.ts
CHANGED
|
@@ -5,7 +5,13 @@ import { makeBridge, emitTurnError } from "./bridge.js";
|
|
|
5
5
|
import { resolveTurnTarget } from "./routing.js";
|
|
6
6
|
import type { QueuedFollowUp, RunningTurn } from "./turns.js";
|
|
7
7
|
import { queuedBytes, queuedFollowUpBytes } from "./turns.js";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
MAX_SESSION_QUEUE,
|
|
10
|
+
QUEUE_MAX_BYTES,
|
|
11
|
+
QUEUE_WARN_BYTES,
|
|
12
|
+
turnLogAttributes,
|
|
13
|
+
type WsHost,
|
|
14
|
+
} from "./host.js";
|
|
9
15
|
|
|
10
16
|
/**
|
|
11
17
|
* Run one session slot: the initial turn, then any queued follow-up turns in
|
|
@@ -20,6 +26,8 @@ export async function runSession(
|
|
|
20
26
|
attachments: ChatImageAttachment[];
|
|
21
27
|
providerId?: string;
|
|
22
28
|
client?: ClientEnvironment;
|
|
29
|
+
/** Client correlation id for a new conversation; echoed on session_info. */
|
|
30
|
+
draftId?: string;
|
|
23
31
|
}
|
|
24
32
|
): Promise<void> {
|
|
25
33
|
const { coordinator } = host;
|
|
@@ -28,10 +36,14 @@ export async function runSession(
|
|
|
28
36
|
try {
|
|
29
37
|
target = await resolveTurnTarget(host.registry, host.catalog, initial.sessionId, initial.providerId);
|
|
30
38
|
} catch (err) {
|
|
39
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
40
|
+
// No turn exists yet — routing failed before one was minted — so the
|
|
41
|
+
// report carries only the session identity the client asked for.
|
|
42
|
+
host.reportTurnFailed("BACKEND_ERROR", { sessionId: initial.sessionId }, message);
|
|
31
43
|
host.sendToClients({
|
|
32
44
|
type: "error",
|
|
33
45
|
code: "BACKEND_ERROR",
|
|
34
|
-
message
|
|
46
|
+
message,
|
|
35
47
|
...(initial.sessionId ? { sessionId: initial.sessionId } : {}),
|
|
36
48
|
});
|
|
37
49
|
return;
|
|
@@ -41,10 +53,11 @@ export async function runSession(
|
|
|
41
53
|
|
|
42
54
|
const { backend, profileId: initialProfileId } = target;
|
|
43
55
|
if (target.droppedPin) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
56
|
+
host.log.emit({
|
|
57
|
+
severityText: "WARN",
|
|
58
|
+
body: "pinned profile unavailable; running on the default profile",
|
|
59
|
+
attributes: { "session.id": initial.sessionId, profile: target.droppedPin },
|
|
60
|
+
});
|
|
48
61
|
host.sendToClients({
|
|
49
62
|
type: "status",
|
|
50
63
|
status: "thinking",
|
|
@@ -55,12 +68,15 @@ export async function runSession(
|
|
|
55
68
|
const turn: RunningTurn = {
|
|
56
69
|
sessionId: initial.sessionId ?? null,
|
|
57
70
|
turnId: crypto.randomUUID(),
|
|
71
|
+
// Only meaningful when the client had no session id to send.
|
|
72
|
+
draftId: initial.sessionId ? null : (initial.draftId ?? null),
|
|
58
73
|
providerId: initialProfileId ?? null,
|
|
59
74
|
backend,
|
|
60
75
|
abortController: new AbortController(),
|
|
61
76
|
timeoutHandle: setTimeout(() => {}, 0),
|
|
62
77
|
queue: [],
|
|
63
78
|
cancelled: false,
|
|
79
|
+
lastResult: null,
|
|
64
80
|
};
|
|
65
81
|
clearTimeout(turn.timeoutHandle);
|
|
66
82
|
coordinator.running.add(turn);
|
|
@@ -82,7 +98,13 @@ export async function runSession(
|
|
|
82
98
|
const abortController = new AbortController();
|
|
83
99
|
turn.abortController = abortController;
|
|
84
100
|
const timeoutHandle = setTimeout(() => {
|
|
85
|
-
|
|
101
|
+
host.log.emit({
|
|
102
|
+
severityText: "WARN",
|
|
103
|
+
body: "turn timed out",
|
|
104
|
+
// Read from the live turn: session_info may have named the session
|
|
105
|
+
// after this timer was armed.
|
|
106
|
+
attributes: { ...turnLogAttributes(turn), "timeout.ms": host.turnTimeoutMs },
|
|
107
|
+
});
|
|
86
108
|
abortController.abort();
|
|
87
109
|
// Reject any pending interactive request for this turn too. A bridge
|
|
88
110
|
// that awaits askUser/approval without racing the abort signal would
|
|
@@ -93,6 +115,8 @@ export async function runSession(
|
|
|
93
115
|
turn.timeoutHandle = timeoutHandle;
|
|
94
116
|
|
|
95
117
|
const bridge = makeBridge(host, turn, text, backend.id);
|
|
118
|
+
const startedAt = Date.now();
|
|
119
|
+
host.reportTurnStarted(turn);
|
|
96
120
|
try {
|
|
97
121
|
await backend.startTurn({
|
|
98
122
|
prompt: text,
|
|
@@ -103,6 +127,14 @@ export async function runSession(
|
|
|
103
127
|
bridge,
|
|
104
128
|
...(client ? { client } : {}),
|
|
105
129
|
});
|
|
130
|
+
// A resolved startTurn is not a successful turn: backends resolve for
|
|
131
|
+
// runtime failures and report them on the terminal result frame, which
|
|
132
|
+
// the bridge recorded on the turn.
|
|
133
|
+
if (turn.lastResult === "error") {
|
|
134
|
+
host.reportTurnFailed("BACKEND_RESULT_ERROR", turn);
|
|
135
|
+
} else {
|
|
136
|
+
host.reportTurnCompleted(turn, Date.now() - startedAt);
|
|
137
|
+
}
|
|
106
138
|
} catch (err) {
|
|
107
139
|
emitTurnError(host, turn, err);
|
|
108
140
|
} finally {
|
|
@@ -118,8 +150,10 @@ export async function runSession(
|
|
|
118
150
|
|
|
119
151
|
if (!turn.cancelled && turn.queue.length > 0) {
|
|
120
152
|
next = turn.queue.shift()!;
|
|
121
|
-
// A queued follow-up is its own turn — give it a fresh identity
|
|
153
|
+
// A queued follow-up is its own turn — give it a fresh identity, and
|
|
154
|
+
// its own terminal disposition.
|
|
122
155
|
turn.turnId = crypto.randomUUID();
|
|
156
|
+
turn.lastResult = null;
|
|
123
157
|
}
|
|
124
158
|
}
|
|
125
159
|
} finally {
|
|
@@ -144,9 +178,10 @@ export async function handleChatMessage(
|
|
|
144
178
|
attachments: ChatImageAttachment[];
|
|
145
179
|
providerId?: string;
|
|
146
180
|
client?: ClientEnvironment;
|
|
181
|
+
draftId?: string;
|
|
147
182
|
}
|
|
148
183
|
): Promise<void> {
|
|
149
|
-
const { text, attachments, sessionId, providerId: requestedProviderId, client } = msg;
|
|
184
|
+
const { text, attachments, sessionId, providerId: requestedProviderId, client, draftId } = msg;
|
|
150
185
|
const { coordinator } = host;
|
|
151
186
|
const runningTurn = sessionId ? coordinator.bySession.get(sessionId) : undefined;
|
|
152
187
|
|
|
@@ -158,11 +193,10 @@ export async function handleChatMessage(
|
|
|
158
193
|
// device snapshot is deliberately not forwarded: a follow-up joins a
|
|
159
194
|
// turn whose system prompt was already built and cannot be revised.
|
|
160
195
|
backend.followUp({ sessionId, prompt: text, attachments }).catch((err) => {
|
|
196
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
197
|
+
host.reportTurnFailed("FOLLOWUP_FAILED", runningTurn, message);
|
|
161
198
|
host.sendToClients(
|
|
162
|
-
withTurnScope(
|
|
163
|
-
{ type: "error", code: "FOLLOWUP_FAILED", message: err instanceof Error ? err.message : String(err) },
|
|
164
|
-
runningTurn
|
|
165
|
-
)
|
|
199
|
+
withTurnScope({ type: "error", code: "FOLLOWUP_FAILED", message }, runningTurn)
|
|
166
200
|
);
|
|
167
201
|
});
|
|
168
202
|
} else {
|
|
@@ -204,7 +238,15 @@ export async function handleChatMessage(
|
|
|
204
238
|
? `Queue is holding ${formatMb(total)} across ${runningTurn.queue.length} messages (limit ${formatMb(QUEUE_MAX_BYTES)}).`
|
|
205
239
|
: undefined;
|
|
206
240
|
if (detail) {
|
|
207
|
-
|
|
241
|
+
host.log.emit({
|
|
242
|
+
severityText: "WARN",
|
|
243
|
+
body: "session follow-up queue is heavy",
|
|
244
|
+
attributes: {
|
|
245
|
+
"session.id": sessionId,
|
|
246
|
+
size: formatMb(total),
|
|
247
|
+
queued: runningTurn.queue.length,
|
|
248
|
+
},
|
|
249
|
+
});
|
|
208
250
|
}
|
|
209
251
|
host.sendToClients(
|
|
210
252
|
withSessionId({ type: "status", status: "queued", ...(detail ? { detail } : {}) }, sessionId)
|
|
@@ -234,5 +276,6 @@ export async function handleChatMessage(
|
|
|
234
276
|
attachments,
|
|
235
277
|
providerId: requestedProviderId,
|
|
236
278
|
...(client ? { client } : {}),
|
|
279
|
+
...(draftId ? { draftId } : {}),
|
|
237
280
|
});
|
|
238
281
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { Database } from "bun:sqlite";
|
|
2
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
2
3
|
import type { ServerResultMessage } from "@schlessera/brain-ui-sdk/protocol";
|
|
3
4
|
|
|
4
5
|
/**
|
|
@@ -42,7 +43,20 @@ const UPSERT_SESSION_SQL = `INSERT INTO sessions (id, title, created_at, last_ac
|
|
|
42
43
|
backend_id = COALESCE(excluded.backend_id, backend_id)`;
|
|
43
44
|
|
|
44
45
|
/** SQLite-backed catalog over the app's own database (injected by createApp). */
|
|
45
|
-
export function createSessionCatalog(db: () => Database): SessionCatalog {
|
|
46
|
+
export function createSessionCatalog(db: () => Database, log?: Logger): SessionCatalog {
|
|
47
|
+
// Persistence stays non-throwing — losing one accounting row must not kill
|
|
48
|
+
// the turn — but never silent: a full disk or locked database would
|
|
49
|
+
// otherwise drop every session record with nothing anywhere.
|
|
50
|
+
const reportWriteFailure = (sessionId: string, err: unknown): void => {
|
|
51
|
+
log?.emit({
|
|
52
|
+
severityText: "WARN",
|
|
53
|
+
body: "session persistence failed",
|
|
54
|
+
attributes: {
|
|
55
|
+
"session.id": sessionId,
|
|
56
|
+
error: err instanceof Error ? err.message : String(err),
|
|
57
|
+
},
|
|
58
|
+
});
|
|
59
|
+
};
|
|
46
60
|
return {
|
|
47
61
|
getStoredProviderId(sessionId) {
|
|
48
62
|
const row = db()
|
|
@@ -72,7 +86,9 @@ export function createSessionCatalog(db: () => Database): SessionCatalog {
|
|
|
72
86
|
providerId,
|
|
73
87
|
backendId
|
|
74
88
|
);
|
|
75
|
-
} catch {
|
|
89
|
+
} catch (err) {
|
|
90
|
+
reportWriteFailure(sessionId, err);
|
|
91
|
+
}
|
|
76
92
|
},
|
|
77
93
|
|
|
78
94
|
persistSession(msg, promptText, providerId, backendId) {
|
|
@@ -90,7 +106,9 @@ export function createSessionCatalog(db: () => Database): SessionCatalog {
|
|
|
90
106
|
providerId,
|
|
91
107
|
backendId
|
|
92
108
|
);
|
|
93
|
-
} catch {
|
|
109
|
+
} catch (err) {
|
|
110
|
+
reportWriteFailure(msg.sessionId, err);
|
|
111
|
+
}
|
|
94
112
|
},
|
|
95
113
|
};
|
|
96
114
|
}
|
package/src/ws/turns.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Logger } from "@opentelemetry/api-logs";
|
|
1
2
|
import type {
|
|
2
3
|
AgentBackend,
|
|
3
4
|
PermissionDecision,
|
|
@@ -54,6 +55,20 @@ export interface RunningTurn {
|
|
|
54
55
|
timeoutHandle: ReturnType<typeof setTimeout>;
|
|
55
56
|
queue: QueuedFollowUp[];
|
|
56
57
|
cancelled: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Terminal disposition of the CURRENT turn's `result` frame, when one has
|
|
60
|
+
* streamed. Backends RESOLVE startTurn for runtime failures (the failure
|
|
61
|
+
* rides the result frame as outcome "error"), so completion accounting must
|
|
62
|
+
* read this — a resolved startTurn alone does not mean the turn succeeded.
|
|
63
|
+
* Reset when a queued follow-up becomes the next turn.
|
|
64
|
+
*/
|
|
65
|
+
lastResult: "success" | "error" | "cancelled" | null;
|
|
66
|
+
/**
|
|
67
|
+
* Correlation id the client minted for this NEW conversation, echoed back on
|
|
68
|
+
* `session_info` so the client can tell its own turn's identity from a
|
|
69
|
+
* background turn's. Null on a resumed session or a client that sent none.
|
|
70
|
+
*/
|
|
71
|
+
draftId: string | null;
|
|
57
72
|
}
|
|
58
73
|
|
|
59
74
|
// Pending interactive requests are tagged with their turn so a per-session
|
|
@@ -93,6 +108,13 @@ export interface PendingMask {
|
|
|
93
108
|
* explicit object with a defined lifecycle.
|
|
94
109
|
*/
|
|
95
110
|
export class TurnCoordinator {
|
|
111
|
+
/**
|
|
112
|
+
* Where collisions are reported. Assigned by the owning WsHost so the
|
|
113
|
+
* coordinator reports through the same consumer as everything else; absent
|
|
114
|
+
* (a bare coordinator in a test) means silence.
|
|
115
|
+
*/
|
|
116
|
+
log?: Logger;
|
|
117
|
+
|
|
96
118
|
readonly running = new Set<RunningTurn>();
|
|
97
119
|
readonly bySession = new Map<string, RunningTurn>();
|
|
98
120
|
startingSessions = 0;
|
|
@@ -173,9 +195,11 @@ export class TurnCoordinator {
|
|
|
173
195
|
): boolean {
|
|
174
196
|
const existing = map.get(id);
|
|
175
197
|
if (existing && existing.turn !== turn) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
198
|
+
this.log?.emit({
|
|
199
|
+
severityText: "ERROR",
|
|
200
|
+
body: "interactive id collision: already pending for another turn",
|
|
201
|
+
attributes: { "request.id": id },
|
|
202
|
+
});
|
|
179
203
|
return true;
|
|
180
204
|
}
|
|
181
205
|
return false;
|