@schlessera/brain-ui-server 0.17.0 → 0.19.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/activity/digest.d.ts +29 -0
- package/dist/activity/digest.d.ts.map +1 -0
- package/dist/activity/digest.js +61 -0
- package/dist/activity/digest.js.map +1 -0
- package/dist/activity/notify.d.ts +62 -0
- package/dist/activity/notify.d.ts.map +1 -0
- package/dist/activity/notify.js +171 -0
- package/dist/activity/notify.js.map +1 -0
- package/dist/activity/push-sender.d.ts +63 -0
- package/dist/activity/push-sender.d.ts.map +1 -0
- package/dist/activity/push-sender.js +131 -0
- package/dist/activity/push-sender.js.map +1 -0
- package/dist/activity/query.d.ts +16 -0
- package/dist/activity/query.d.ts.map +1 -0
- package/dist/activity/query.js +128 -0
- package/dist/activity/query.js.map +1 -0
- package/dist/activity/recorder.d.ts +48 -0
- package/dist/activity/recorder.d.ts.map +1 -0
- package/dist/activity/recorder.js +205 -0
- package/dist/activity/recorder.js.map +1 -0
- package/dist/activity/runtime.d.ts +31 -0
- package/dist/activity/runtime.d.ts.map +1 -0
- package/dist/activity/runtime.js +109 -0
- package/dist/activity/runtime.js.map +1 -0
- package/dist/activity/span-sink.d.ts +22 -0
- package/dist/activity/span-sink.d.ts.map +1 -0
- package/dist/activity/span-sink.js +138 -0
- package/dist/activity/span-sink.js.map +1 -0
- package/dist/activity/store.d.ts +207 -0
- package/dist/activity/store.d.ts.map +1 -0
- package/dist/activity/store.js +440 -0
- package/dist/activity/store.js.map +1 -0
- package/dist/activity/stream.d.ts +45 -0
- package/dist/activity/stream.d.ts.map +1 -0
- package/dist/activity/stream.js +312 -0
- package/dist/activity/stream.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +20 -1
- package/dist/app.js.map +1 -1
- package/dist/config/env.d.ts +5 -0
- package/dist/config/env.d.ts.map +1 -1
- package/dist/config/env.js +12 -0
- package/dist/config/env.js.map +1 -1
- package/dist/cron/scheduler.d.ts.map +1 -1
- package/dist/cron/scheduler.js +20 -5
- package/dist/cron/scheduler.js.map +1 -1
- package/dist/db/client.d.ts.map +1 -1
- package/dist/db/client.js +32 -6
- package/dist/db/client.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/routes/activity.d.ts +20 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +300 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/push.d.ts +6 -0
- package/dist/routes/push.d.ts.map +1 -0
- package/dist/routes/push.js +73 -0
- package/dist/routes/push.js.map +1 -0
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +20 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/ws/bridge.d.ts +2 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +45 -3
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +41 -5
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +11 -0
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/host.d.ts +16 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +2 -0
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +25 -1
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/turns.d.ts +22 -2
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +13 -0
- package/dist/ws/turns.js.map +1 -1
- package/migrations/007_activity.sql +114 -0
- package/migrations/008_push_subscriptions.sql +24 -0
- package/package.json +5 -3
- package/src/activity/digest.ts +92 -0
- package/src/activity/notify.ts +285 -0
- package/src/activity/push-sender.ts +215 -0
- package/src/activity/query.ts +160 -0
- package/src/activity/recorder.ts +261 -0
- package/src/activity/runtime.ts +141 -0
- package/src/activity/span-sink.ts +152 -0
- package/src/activity/store.ts +774 -0
- package/src/activity/stream.ts +366 -0
- package/src/app.ts +21 -1
- package/src/config/env.ts +19 -0
- package/src/cron/scheduler.ts +28 -8
- package/src/db/client.ts +31 -6
- package/src/index.ts +22 -0
- package/src/routes/activity.ts +363 -0
- package/src/routes/push.ts +90 -0
- package/src/routes/sessions.ts +24 -4
- package/src/ws/bridge.ts +47 -3
- package/src/ws/connection.ts +57 -5
- package/src/ws/dispatch.ts +13 -0
- package/src/ws/host.ts +18 -0
- package/src/ws/run-session.ts +32 -1
- package/src/ws/turns.ts +29 -1
package/src/ws/dispatch.ts
CHANGED
|
@@ -177,6 +177,19 @@ export async function handleClientMessage(
|
|
|
177
177
|
break;
|
|
178
178
|
}
|
|
179
179
|
|
|
180
|
+
case "activity_subscribe": {
|
|
181
|
+
// View-scoped opt-in: without a subscription this connection never
|
|
182
|
+
// receives an activity frame. No turn correlation — subscriptions are
|
|
183
|
+
// connection state, not turn state.
|
|
184
|
+
host.activity?.stream.handleSubscribe(ws, msg);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
case "activity_unsubscribe": {
|
|
189
|
+
host.activity?.stream.handleUnsubscribe(ws, msg);
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
|
|
180
193
|
case "session_resume": {
|
|
181
194
|
host.sendMessage(ws, {
|
|
182
195
|
type: "session_info",
|
package/src/ws/host.ts
CHANGED
|
@@ -5,6 +5,16 @@ import type { SessionCatalog } from "./session-catalog.js";
|
|
|
5
5
|
import type { BackendRegistry } from "../agent/backend.js";
|
|
6
6
|
import { createSilentObservability, type Observability } from "../observability/index.js";
|
|
7
7
|
import { FrameRateLimiter } from "./rate-limit.js";
|
|
8
|
+
import type { ActivityStore } from "../activity/store.js";
|
|
9
|
+
import type { ActivityStream } from "../activity/stream.js";
|
|
10
|
+
|
|
11
|
+
/** The activity record and its live stream, when the host records activity. */
|
|
12
|
+
export interface ActivityRuntime {
|
|
13
|
+
store: ActivityStore;
|
|
14
|
+
stream: ActivityStream;
|
|
15
|
+
/** Read seam for the agent-facing query tool (bridge.queryActivity). */
|
|
16
|
+
query?: (query: import("@schlessera/brain-ui-sdk/server").ActivityQuery) => Record<string, unknown>;
|
|
17
|
+
}
|
|
8
18
|
|
|
9
19
|
/** Host-side turn timeout. The backend no longer times out — the host owns it. */
|
|
10
20
|
const DEFAULT_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
|
|
@@ -65,6 +75,12 @@ export interface WsHostOptions {
|
|
|
65
75
|
* fronting the socket with its own limiter wants.
|
|
66
76
|
*/
|
|
67
77
|
wsRate?: { ratePerSecond: number; burst: number };
|
|
78
|
+
/**
|
|
79
|
+
* Activity recording (span store + live stream). Optional: a host without
|
|
80
|
+
* one records nothing and never sends activity frames — which is also what
|
|
81
|
+
* most existing tests want.
|
|
82
|
+
*/
|
|
83
|
+
activity?: ActivityRuntime;
|
|
68
84
|
}
|
|
69
85
|
|
|
70
86
|
/** Identity of one turn, as it appears on a log record. */
|
|
@@ -102,6 +118,7 @@ export class WsHost {
|
|
|
102
118
|
maxConcurrentSessions: () => number;
|
|
103
119
|
readonly observability: Observability;
|
|
104
120
|
readonly wsRate: { ratePerSecond: number; burst: number } | null;
|
|
121
|
+
readonly activity: ActivityRuntime | null;
|
|
105
122
|
/** Scoped instruments, resolved once — `[ws]` is the existing log prefix. */
|
|
106
123
|
readonly log: ReturnType<Observability["logger"]>;
|
|
107
124
|
private readonly framesDropped: ReturnType<
|
|
@@ -130,6 +147,7 @@ export class WsHost {
|
|
|
130
147
|
this.observability = options.observability ?? createSilentObservability();
|
|
131
148
|
this.wsRate =
|
|
132
149
|
options.wsRate && options.wsRate.ratePerSecond > 0 ? options.wsRate : null;
|
|
150
|
+
this.activity = options.activity ?? null;
|
|
133
151
|
this.log = this.observability.logger("ws");
|
|
134
152
|
const meter = this.observability.meter("ws");
|
|
135
153
|
this.framesDropped = meter.createCounter("ws.frames.dropped", {
|
package/src/ws/run-session.ts
CHANGED
|
@@ -2,6 +2,7 @@ import type { ChatImageAttachment, ClientEnvironment } from "@schlessera/brain-u
|
|
|
2
2
|
import type { WSContext } from "./clients.js";
|
|
3
3
|
import { withSessionId, withTurnScope } from "./frames.js";
|
|
4
4
|
import { makeBridge, emitTurnError } from "./bridge.js";
|
|
5
|
+
import { createTurnRecorder, type TurnRecorder } from "../activity/recorder.js";
|
|
5
6
|
import { resolveTurnTarget } from "./routing.js";
|
|
6
7
|
import type { QueuedFollowUp, RunningTurn } from "./turns.js";
|
|
7
8
|
import { queuedBytes, queuedFollowUpBytes } from "./turns.js";
|
|
@@ -114,7 +115,19 @@ export async function runSession(
|
|
|
114
115
|
}, host.turnTimeoutMs);
|
|
115
116
|
turn.timeoutHandle = timeoutHandle;
|
|
116
117
|
|
|
117
|
-
|
|
118
|
+
// One recorder per turn identity: a queued follow-up re-mints turnId
|
|
119
|
+
// and gets its own run in the activity record.
|
|
120
|
+
const recorder: TurnRecorder | undefined = host.activity
|
|
121
|
+
? createTurnRecorder(
|
|
122
|
+
{
|
|
123
|
+
store: host.activity.store,
|
|
124
|
+
onWrite: () => host.activity!.stream.pump(),
|
|
125
|
+
log: host.log,
|
|
126
|
+
},
|
|
127
|
+
{ turnId: turn.turnId, sessionId: turn.sessionId }
|
|
128
|
+
)
|
|
129
|
+
: undefined;
|
|
130
|
+
const bridge = makeBridge(host, turn, text, backend.id, recorder);
|
|
118
131
|
const startedAt = Date.now();
|
|
119
132
|
host.reportTurnStarted(turn);
|
|
120
133
|
try {
|
|
@@ -124,6 +137,10 @@ export async function runSession(
|
|
|
124
137
|
sessionId: resumeId,
|
|
125
138
|
profileId,
|
|
126
139
|
signal: abortController.signal,
|
|
140
|
+
// The budget this very timer enforces, so the backend can put the
|
|
141
|
+
// real number in front of the model instead of it finding the cap
|
|
142
|
+
// mid-flight.
|
|
143
|
+
turnBudgetMs: host.turnTimeoutMs,
|
|
127
144
|
bridge,
|
|
128
145
|
...(client ? { client } : {}),
|
|
129
146
|
});
|
|
@@ -135,8 +152,22 @@ export async function runSession(
|
|
|
135
152
|
} else {
|
|
136
153
|
host.reportTurnCompleted(turn, Date.now() - startedAt);
|
|
137
154
|
}
|
|
155
|
+
// Terminal precedence: cancellation and timeout are host-owned facts;
|
|
156
|
+
// otherwise the recorder refines from the buffered result frame.
|
|
157
|
+
recorder?.finish(
|
|
158
|
+
turn.cancelled
|
|
159
|
+
? "cancelled"
|
|
160
|
+
: abortController.signal.aborted
|
|
161
|
+
? "timeout"
|
|
162
|
+
: turn.lastResult === "error"
|
|
163
|
+
? "error"
|
|
164
|
+
: "success"
|
|
165
|
+
);
|
|
138
166
|
} catch (err) {
|
|
139
167
|
emitTurnError(host, turn, err);
|
|
168
|
+
recorder?.finish(
|
|
169
|
+
turn.cancelled ? "cancelled" : abortController.signal.aborted ? "timeout" : "error"
|
|
170
|
+
);
|
|
140
171
|
} finally {
|
|
141
172
|
clearTimeout(timeoutHandle);
|
|
142
173
|
}
|
package/src/ws/turns.ts
CHANGED
|
@@ -2,10 +2,15 @@ import type { Logger } from "@opentelemetry/api-logs";
|
|
|
2
2
|
import type {
|
|
3
3
|
AgentBackend,
|
|
4
4
|
PermissionDecision,
|
|
5
|
+
PermissionRequest,
|
|
5
6
|
AskUserResult,
|
|
6
7
|
LocationFix,
|
|
7
8
|
} from "@schlessera/brain-ui-sdk/server";
|
|
8
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AskUserQuestion,
|
|
11
|
+
ChatImageAttachment,
|
|
12
|
+
ClientEnvironment,
|
|
13
|
+
} from "@schlessera/brain-ui-sdk/protocol";
|
|
9
14
|
|
|
10
15
|
export interface QueuedFollowUp {
|
|
11
16
|
text: string;
|
|
@@ -77,12 +82,21 @@ export interface RunningTurn {
|
|
|
77
82
|
export interface PendingApproval {
|
|
78
83
|
turn: RunningTurn;
|
|
79
84
|
turnId: string;
|
|
85
|
+
/**
|
|
86
|
+
* The original request, kept so the card can be RE-DELIVERED to a client
|
|
87
|
+
* that connects later. On a phone the socket drops every time the screen
|
|
88
|
+
* locks; an approval must survive that and reappear, not silently die.
|
|
89
|
+
*/
|
|
90
|
+
request: PermissionRequest;
|
|
80
91
|
resolve: (decision: PermissionDecision) => void;
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
export interface PendingAskUser {
|
|
84
95
|
turn: RunningTurn;
|
|
85
96
|
turnId: string;
|
|
97
|
+
/** Kept for re-delivery on reconnect, like PendingApproval.request. */
|
|
98
|
+
requestId: string;
|
|
99
|
+
questions: AskUserQuestion[];
|
|
86
100
|
resolve: (result: AskUserResult) => void;
|
|
87
101
|
reject: (err: Error) => void;
|
|
88
102
|
}
|
|
@@ -168,6 +182,20 @@ export class TurnCoordinator {
|
|
|
168
182
|
p.reject(new Error(reason));
|
|
169
183
|
this.pendingAskUser.delete(id);
|
|
170
184
|
}
|
|
185
|
+
this.drainClientBoundForTurn(turn, reason);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Reject only the requests that NEED a live client at this instant —
|
|
190
|
+
* location and mask, whose sibling handlers already fail fast with no
|
|
191
|
+
* client attached. Approvals and ask-user cards deliberately survive a
|
|
192
|
+
* disconnect: on a phone the socket drops at every screen lock, and before
|
|
193
|
+
* this split a pending approval was silently denied the moment the screen
|
|
194
|
+
* went dark ("Client disconnected") while one raised DURING the dark parked
|
|
195
|
+
* until the turn timeout. Both now hold, bounded by the turn timeout, and
|
|
196
|
+
* re-deliver on reconnect.
|
|
197
|
+
*/
|
|
198
|
+
drainClientBoundForTurn(turn: RunningTurn, reason: string): void {
|
|
171
199
|
for (const [id, p] of this.pendingLocation) {
|
|
172
200
|
if (p.turn !== turn) continue;
|
|
173
201
|
p.reject(new Error(reason));
|