@schlessera/brain-ui-server 0.16.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 +1 -0
- package/dist/agent/backend.d.ts +31 -10
- package/dist/agent/backend.d.ts.map +1 -1
- package/dist/agent/backend.js +86 -26
- package/dist/agent/backend.js.map +1 -1
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +47 -16
- 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 +19 -5
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +43 -11
- package/dist/config/env.js.map +1 -1
- package/dist/cron/scheduler.d.ts +17 -0
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +33 -6
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/settings.d.ts +1 -1
- package/dist/db/settings.d.ts.map +1 -1
- package/dist/db/settings.js +2 -2
- package/dist/db/settings.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +46 -1
- package/dist/middleware/auth.js.map +1 -1
- package/dist/routes/health.d.ts +4 -14
- package/dist/routes/health.d.ts.map +1 -1
- package/dist/routes/health.js +22 -6
- package/dist/routes/health.js.map +1 -1
- package/dist/routes/share.d.ts.map +1 -1
- package/dist/routes/share.js +2 -2
- package/dist/routes/share.js.map +1 -1
- package/dist/voice/keyterm-builder.js +2 -2
- package/dist/voice/keyterm-builder.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +17 -2
- 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 +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +11 -2
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +3 -1
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +36 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +93 -4
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +27 -5
- 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 +8 -0
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js.map +1 -1
- package/package.json +7 -6
- package/src/agent/backend.ts +123 -29
- package/src/app.ts +55 -16
- package/src/config/env-core.ts +93 -0
- package/src/config/env.ts +61 -17
- package/src/cron/scheduler.ts +48 -14
- package/src/db/settings.ts +2 -2
- package/src/index.ts +6 -0
- package/src/middleware/auth.ts +50 -1
- package/src/routes/health.ts +22 -6
- package/src/routes/share.ts +11 -7
- package/src/voice/keyterm-builder.ts +2 -2
- package/src/ws/bridge.ts +17 -5
- package/src/ws/clients.ts +24 -6
- package/src/ws/connection.ts +11 -2
- package/src/ws/dispatch.ts +3 -1
- package/src/ws/host.ts +115 -6
- package/src/ws/run-session.ts +32 -8
- package/src/ws/session-catalog.ts +21 -3
- package/src/ws/turns.ts +8 -0
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
|
|
@@ -30,10 +36,14 @@ export async function runSession(
|
|
|
30
36
|
try {
|
|
31
37
|
target = await resolveTurnTarget(host.registry, host.catalog, initial.sessionId, initial.providerId);
|
|
32
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);
|
|
33
43
|
host.sendToClients({
|
|
34
44
|
type: "error",
|
|
35
45
|
code: "BACKEND_ERROR",
|
|
36
|
-
message
|
|
46
|
+
message,
|
|
37
47
|
...(initial.sessionId ? { sessionId: initial.sessionId } : {}),
|
|
38
48
|
});
|
|
39
49
|
return;
|
|
@@ -66,6 +76,7 @@ export async function runSession(
|
|
|
66
76
|
timeoutHandle: setTimeout(() => {}, 0),
|
|
67
77
|
queue: [],
|
|
68
78
|
cancelled: false,
|
|
79
|
+
lastResult: null,
|
|
69
80
|
};
|
|
70
81
|
clearTimeout(turn.timeoutHandle);
|
|
71
82
|
coordinator.running.add(turn);
|
|
@@ -90,7 +101,9 @@ export async function runSession(
|
|
|
90
101
|
host.log.emit({
|
|
91
102
|
severityText: "WARN",
|
|
92
103
|
body: "turn timed out",
|
|
93
|
-
|
|
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 },
|
|
94
107
|
});
|
|
95
108
|
abortController.abort();
|
|
96
109
|
// Reject any pending interactive request for this turn too. A bridge
|
|
@@ -102,6 +115,8 @@ export async function runSession(
|
|
|
102
115
|
turn.timeoutHandle = timeoutHandle;
|
|
103
116
|
|
|
104
117
|
const bridge = makeBridge(host, turn, text, backend.id);
|
|
118
|
+
const startedAt = Date.now();
|
|
119
|
+
host.reportTurnStarted(turn);
|
|
105
120
|
try {
|
|
106
121
|
await backend.startTurn({
|
|
107
122
|
prompt: text,
|
|
@@ -112,6 +127,14 @@ export async function runSession(
|
|
|
112
127
|
bridge,
|
|
113
128
|
...(client ? { client } : {}),
|
|
114
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
|
+
}
|
|
115
138
|
} catch (err) {
|
|
116
139
|
emitTurnError(host, turn, err);
|
|
117
140
|
} finally {
|
|
@@ -127,8 +150,10 @@ export async function runSession(
|
|
|
127
150
|
|
|
128
151
|
if (!turn.cancelled && turn.queue.length > 0) {
|
|
129
152
|
next = turn.queue.shift()!;
|
|
130
|
-
// 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.
|
|
131
155
|
turn.turnId = crypto.randomUUID();
|
|
156
|
+
turn.lastResult = null;
|
|
132
157
|
}
|
|
133
158
|
}
|
|
134
159
|
} finally {
|
|
@@ -168,11 +193,10 @@ export async function handleChatMessage(
|
|
|
168
193
|
// device snapshot is deliberately not forwarded: a follow-up joins a
|
|
169
194
|
// turn whose system prompt was already built and cannot be revised.
|
|
170
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);
|
|
171
198
|
host.sendToClients(
|
|
172
|
-
withTurnScope(
|
|
173
|
-
{ type: "error", code: "FOLLOWUP_FAILED", message: err instanceof Error ? err.message : String(err) },
|
|
174
|
-
runningTurn
|
|
175
|
-
)
|
|
199
|
+
withTurnScope({ type: "error", code: "FOLLOWUP_FAILED", message }, runningTurn)
|
|
176
200
|
);
|
|
177
201
|
});
|
|
178
202
|
} else {
|
|
@@ -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
|
@@ -55,6 +55,14 @@ export interface RunningTurn {
|
|
|
55
55
|
timeoutHandle: ReturnType<typeof setTimeout>;
|
|
56
56
|
queue: QueuedFollowUp[];
|
|
57
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;
|
|
58
66
|
/**
|
|
59
67
|
* Correlation id the client minted for this NEW conversation, echoed back on
|
|
60
68
|
* `session_info` so the client can tell its own turn's identity from a
|