@schlessera/brain-ui-server 0.34.1 → 0.35.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 +28 -0
- package/dist/activity/notify.d.ts.map +1 -1
- package/dist/activity/notify.js +18 -8
- package/dist/activity/notify.js.map +1 -1
- package/dist/activity/push-sender.d.ts +7 -3
- package/dist/activity/push-sender.d.ts.map +1 -1
- package/dist/activity/push-sender.js +45 -11
- package/dist/activity/push-sender.js.map +1 -1
- package/dist/activity/query.d.ts.map +1 -1
- package/dist/activity/query.js +8 -0
- package/dist/activity/query.js.map +1 -1
- package/dist/activity/recorder.d.ts +11 -1
- package/dist/activity/recorder.d.ts.map +1 -1
- package/dist/activity/recorder.js +52 -2
- package/dist/activity/recorder.js.map +1 -1
- package/dist/activity/runtime.d.ts.map +1 -1
- package/dist/activity/runtime.js +2 -5
- package/dist/activity/runtime.js.map +1 -1
- package/dist/activity/sql.d.ts +3 -3
- package/dist/activity/sql.d.ts.map +1 -1
- package/dist/activity/sql.js +7 -5
- package/dist/activity/sql.js.map +1 -1
- package/dist/activity/store.d.ts +7 -0
- package/dist/activity/store.d.ts.map +1 -1
- package/dist/activity/store.js +15 -3
- package/dist/activity/store.js.map +1 -1
- package/dist/activity/stream.d.ts +3 -1
- package/dist/activity/stream.d.ts.map +1 -1
- package/dist/activity/stream.js +32 -17
- package/dist/activity/stream.js.map +1 -1
- package/dist/app-env.d.ts +8 -0
- package/dist/app-env.d.ts.map +1 -0
- package/dist/app-env.js +2 -0
- package/dist/app-env.js.map +1 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +28 -3
- package/dist/app.js.map +1 -1
- package/dist/brain/client.d.ts +4 -2
- package/dist/brain/client.d.ts.map +1 -1
- package/dist/brain/client.js +60 -20
- package/dist/brain/client.js.map +1 -1
- package/dist/brain/types.d.ts +9 -0
- package/dist/brain/types.d.ts.map +1 -1
- package/dist/cron/run-job.d.ts.map +1 -1
- package/dist/cron/run-job.js +60 -10
- package/dist/cron/run-job.js.map +1 -1
- package/dist/db/principals.d.ts +72 -0
- package/dist/db/principals.d.ts.map +1 -0
- package/dist/db/principals.js +294 -0
- package/dist/db/principals.js.map +1 -0
- package/dist/files/walker.d.ts.map +1 -1
- package/dist/files/walker.js +6 -1
- package/dist/files/walker.js.map +1 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/middleware/auth.d.ts +37 -9
- package/dist/middleware/auth.d.ts.map +1 -1
- package/dist/middleware/auth.js +148 -74
- package/dist/middleware/auth.js.map +1 -1
- package/dist/middleware/passkeys.d.ts +9 -7
- package/dist/middleware/passkeys.d.ts.map +1 -1
- package/dist/middleware/passkeys.js +25 -7
- package/dist/middleware/passkeys.js.map +1 -1
- package/dist/middleware/principals.d.ts +18 -0
- package/dist/middleware/principals.d.ts.map +1 -0
- package/dist/middleware/principals.js +138 -0
- package/dist/middleware/principals.js.map +1 -0
- package/dist/middleware/require-owner.d.ts +5 -0
- package/dist/middleware/require-owner.d.ts.map +1 -0
- package/dist/middleware/require-owner.js +10 -0
- package/dist/middleware/require-owner.js.map +1 -0
- package/dist/observability/loggers.js +1 -1
- package/dist/observability/loggers.js.map +1 -1
- package/dist/routes/activity.d.ts.map +1 -1
- package/dist/routes/activity.js +3 -0
- package/dist/routes/activity.js.map +1 -1
- package/dist/routes/brain.d.ts.map +1 -1
- package/dist/routes/brain.js +102 -63
- package/dist/routes/brain.js.map +1 -1
- package/dist/routes/files.d.ts.map +1 -1
- package/dist/routes/files.js +7 -2
- package/dist/routes/files.js.map +1 -1
- package/dist/routes/push.d.ts +61 -2
- package/dist/routes/push.d.ts.map +1 -1
- package/dist/routes/push.js +8 -3
- package/dist/routes/push.js.map +1 -1
- package/dist/routes/sessions.d.ts.map +1 -1
- package/dist/routes/sessions.js +30 -4
- package/dist/routes/sessions.js.map +1 -1
- package/dist/share/staging.d.ts.map +1 -1
- package/dist/share/staging.js +11 -4
- package/dist/share/staging.js.map +1 -1
- package/dist/ws/bridge.d.ts.map +1 -1
- package/dist/ws/bridge.js +6 -2
- package/dist/ws/bridge.js.map +1 -1
- package/dist/ws/clients.d.ts +9 -2
- package/dist/ws/clients.d.ts.map +1 -1
- package/dist/ws/clients.js +44 -6
- package/dist/ws/clients.js.map +1 -1
- package/dist/ws/connection.d.ts +7 -4
- package/dist/ws/connection.d.ts.map +1 -1
- package/dist/ws/connection.js +83 -12
- package/dist/ws/connection.js.map +1 -1
- package/dist/ws/dispatch.d.ts +12 -1
- package/dist/ws/dispatch.d.ts.map +1 -1
- package/dist/ws/dispatch.js +46 -6
- package/dist/ws/dispatch.js.map +1 -1
- package/dist/ws/history.d.ts.map +1 -1
- package/dist/ws/history.js +7 -5
- package/dist/ws/history.js.map +1 -1
- package/dist/ws/host.d.ts +22 -0
- package/dist/ws/host.d.ts.map +1 -1
- package/dist/ws/host.js +39 -1
- package/dist/ws/host.js.map +1 -1
- package/dist/ws/run-session.d.ts +7 -7
- package/dist/ws/run-session.d.ts.map +1 -1
- package/dist/ws/run-session.js +208 -52
- package/dist/ws/run-session.js.map +1 -1
- package/dist/ws/shrink.d.ts +3 -1
- package/dist/ws/shrink.d.ts.map +1 -1
- package/dist/ws/shrink.js +46 -16
- package/dist/ws/shrink.js.map +1 -1
- package/dist/ws/turns.d.ts +58 -3
- package/dist/ws/turns.d.ts.map +1 -1
- package/dist/ws/turns.js +101 -2
- package/dist/ws/turns.js.map +1 -1
- package/migrations/011_principals.sql +13 -0
- package/migrations/012_activity_principals.sql +12 -0
- package/migrations/013_push_subscription_principals.sql +7 -0
- package/migrations/014_invalidate_legacy_sessions.sql +14 -0
- package/package.json +3 -3
- package/src/activity/notify.ts +16 -7
- package/src/activity/push-sender.ts +79 -16
- package/src/activity/query.ts +8 -0
- package/src/activity/recorder.ts +68 -3
- package/src/activity/runtime.ts +2 -5
- package/src/activity/sql.ts +7 -5
- package/src/activity/store.ts +32 -1
- package/src/activity/stream.ts +37 -17
- package/src/app-env.ts +8 -0
- package/src/app.ts +33 -4
- package/src/brain/client.ts +61 -20
- package/src/brain/types.ts +10 -0
- package/src/cron/run-job.ts +41 -17
- package/src/db/principals.ts +452 -0
- package/src/files/walker.ts +5 -1
- package/src/index.ts +6 -1
- package/src/middleware/auth.ts +241 -81
- package/src/middleware/passkeys.ts +35 -14
- package/src/middleware/principals.ts +189 -0
- package/src/middleware/require-owner.ts +13 -0
- package/src/observability/loggers.ts +1 -1
- package/src/routes/activity.ts +3 -0
- package/src/routes/brain.ts +107 -70
- package/src/routes/files.ts +10 -5
- package/src/routes/push.ts +11 -5
- package/src/routes/sessions.ts +45 -20
- package/src/share/staging.ts +11 -4
- package/src/ws/bridge.ts +14 -2
- package/src/ws/clients.ts +52 -7
- package/src/ws/connection.ts +98 -20
- package/src/ws/dispatch.ts +66 -6
- package/src/ws/history.ts +7 -5
- package/src/ws/host.ts +59 -1
- package/src/ws/run-session.ts +247 -74
- package/src/ws/shrink.ts +48 -14
- package/src/ws/turns.ts +136 -5
package/dist/ws/turns.d.ts
CHANGED
|
@@ -1,11 +1,28 @@
|
|
|
1
1
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
2
2
|
import type { AgentBackend, PermissionDecision, PermissionRequest, AskUserResult, LocationFix } from "@schlessera/brain-ui-sdk/server";
|
|
3
3
|
import type { AskUserQuestion, ChatImageAttachment, ClientEnvironment } from "@schlessera/brain-ui-sdk/protocol";
|
|
4
|
+
import type { TurnRecorder } from "../activity/recorder.js";
|
|
5
|
+
declare const authorizationContextBrand: unique symbol;
|
|
6
|
+
/** Mutable admission decision shared by every asynchronous path from one socket. */
|
|
7
|
+
export interface AuthorizationContext {
|
|
8
|
+
readonly [authorizationContextBrand]: true;
|
|
9
|
+
readonly principalId: string;
|
|
10
|
+
readonly expiresAt: number;
|
|
11
|
+
valid: boolean;
|
|
12
|
+
/** Keep this context discoverable until the returned release is called. */
|
|
13
|
+
retain(): () => void;
|
|
14
|
+
/** Release the reference acquired when the coordinator opened this context. */
|
|
15
|
+
release(): void;
|
|
16
|
+
}
|
|
4
17
|
export interface QueuedFollowUp {
|
|
18
|
+
principalId: string;
|
|
19
|
+
authorization: AuthorizationContext;
|
|
5
20
|
text: string;
|
|
6
21
|
attachments: ChatImageAttachment[];
|
|
7
22
|
/** Device snapshot taken when the message was sent, not when it runs. */
|
|
8
23
|
client?: ClientEnvironment;
|
|
24
|
+
/** Queue-owned authorization lease, transferred to the runner on dequeue. */
|
|
25
|
+
releaseAuthorization: () => void;
|
|
9
26
|
}
|
|
10
27
|
/**
|
|
11
28
|
* Bytes one queued entry keeps alive in this process until its turn runs.
|
|
@@ -18,15 +35,25 @@ export interface QueuedFollowUp {
|
|
|
18
35
|
* on the wire.) The client snapshot is a handful of short fields and is not
|
|
19
36
|
* worth walking.
|
|
20
37
|
*/
|
|
21
|
-
export declare function queuedFollowUpBytes(entry: QueuedFollowUp): number;
|
|
38
|
+
export declare function queuedFollowUpBytes(entry: Pick<QueuedFollowUp, "text" | "attachments">): number;
|
|
22
39
|
/** Total bytes currently parked in a session's follow-up queue. */
|
|
23
|
-
export declare function queuedBytes(turn: RunningTurn): number;
|
|
40
|
+
export declare function queuedBytes(turn: Pick<RunningTurn, "queue">): number;
|
|
24
41
|
/**
|
|
25
42
|
* One RUNNING session slot. `abortController`/`timeoutHandle` belong to the
|
|
26
43
|
* turn currently executing; a queued follow-up runs as the next turn in the
|
|
27
44
|
* same slot (same sessionId) after the current one resolves.
|
|
28
45
|
*/
|
|
29
46
|
export interface RunningTurn {
|
|
47
|
+
/** Principal responsible for the CURRENT turn in this session slot. */
|
|
48
|
+
principalId: string;
|
|
49
|
+
authorization: AuthorizationContext;
|
|
50
|
+
/** Activity recorder for the CURRENT turn, once startup reaches the backend. */
|
|
51
|
+
recorder?: TurnRecorder;
|
|
52
|
+
/**
|
|
53
|
+
* Cancelling actors accepted before the recorder exists (while billing is
|
|
54
|
+
* resolving). Drained into immutable activity events at recorder creation.
|
|
55
|
+
*/
|
|
56
|
+
pendingCancellationPrincipalIds: string[];
|
|
30
57
|
sessionId: string | null;
|
|
31
58
|
/** Host-minted id of the CURRENT turn in this slot; re-minted per queued follow-up. */
|
|
32
59
|
turnId: string;
|
|
@@ -60,7 +87,10 @@ export interface PendingApproval {
|
|
|
60
87
|
* locks; an approval must survive that and reappear, not silently die.
|
|
61
88
|
*/
|
|
62
89
|
request: PermissionRequest;
|
|
63
|
-
resolve: (decision: PermissionDecision
|
|
90
|
+
resolve: (decision: PermissionDecision, response?: {
|
|
91
|
+
principalId: string;
|
|
92
|
+
always?: boolean;
|
|
93
|
+
}) => void;
|
|
64
94
|
}
|
|
65
95
|
export interface PendingAskUser {
|
|
66
96
|
turn: RunningTurn;
|
|
@@ -99,20 +129,44 @@ export declare class TurnCoordinator {
|
|
|
99
129
|
readonly running: Set<RunningTurn>;
|
|
100
130
|
readonly bySession: Map<string, RunningTurn>;
|
|
101
131
|
startingSessions: number;
|
|
132
|
+
/** Reserve a known session before asynchronous backend routing. */
|
|
133
|
+
readonly startingBySession: Map<string, {
|
|
134
|
+
queue: QueuedFollowUp[];
|
|
135
|
+
cancelled: boolean;
|
|
136
|
+
}>;
|
|
102
137
|
readonly pendingApprovals: Map<string, PendingApproval>;
|
|
103
138
|
readonly pendingAskUser: Map<string, PendingAskUser>;
|
|
104
139
|
readonly pendingLocation: Map<string, PendingLocation>;
|
|
105
140
|
readonly pendingMask: Map<string, PendingMask>;
|
|
141
|
+
/** Every authorization context with at least one live owner or async lease. */
|
|
142
|
+
readonly authorizationRegistry: Map<AuthorizationContext, number>;
|
|
106
143
|
private locationCounter;
|
|
107
144
|
private maskCounter;
|
|
108
145
|
nextLocationRequestId(): string;
|
|
109
146
|
nextMaskRequestId(): string;
|
|
110
147
|
/** True while any session has a running turn. */
|
|
111
148
|
isTurnActive(): boolean;
|
|
149
|
+
/** Construct and synchronously register a context with its initial reference. */
|
|
150
|
+
openAuthorization(input: {
|
|
151
|
+
principalId: string;
|
|
152
|
+
expiresAt: number;
|
|
153
|
+
valid: boolean;
|
|
154
|
+
}): AuthorizationContext;
|
|
155
|
+
/** Add principals whose registered authorization context has expired. */
|
|
156
|
+
collectExpiredPrincipalIds(now: number, ids: Set<string>): void;
|
|
157
|
+
/** Invalidate every registered context belonging to one of these principals. */
|
|
158
|
+
invalidateAuthorizations(principalIds: ReadonlySet<string>): void;
|
|
159
|
+
/**
|
|
160
|
+
* Record revocation without aborting running work. Unstarted queued work is
|
|
161
|
+
* removed per sender, while a current turn merely carries the invalid marker.
|
|
162
|
+
*/
|
|
163
|
+
revokePrincipals(principalIds: ReadonlySet<string>): RunningTurn[];
|
|
112
164
|
/** Cancel every running turn (used on shutdown). */
|
|
113
165
|
cancelAll(reason: string): boolean;
|
|
114
166
|
/** Cancel a session's current turn and drop its queued follow-ups. */
|
|
115
167
|
cancelTurn(turn: RunningTurn, reason: string): void;
|
|
168
|
+
/** Record now, or retain the actor across the pre-recorder startup window. */
|
|
169
|
+
recordCancellation(turn: RunningTurn, principalId: string): void;
|
|
116
170
|
/** Resolve/reject every pending interactive request belonging to one turn. */
|
|
117
171
|
drainPendingForTurn(turn: RunningTurn, reason: string): void;
|
|
118
172
|
/**
|
|
@@ -140,4 +194,5 @@ export declare class TurnCoordinator {
|
|
|
140
194
|
/** Test seam: abort everything and clear all turn/pending state. */
|
|
141
195
|
reset(): void;
|
|
142
196
|
}
|
|
197
|
+
export {};
|
|
143
198
|
//# sourceMappingURL=turns.d.ts.map
|
package/dist/ws/turns.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,WAAW,EACZ,MAAM,iCAAiC,CAAC;AACzC,OAAO,KAAK,EACV,eAAe,EACf,mBAAmB,EACnB,iBAAiB,EAClB,MAAM,mCAAmC,CAAC;
|
|
1
|
+
{"version":3,"file":"turns.d.ts","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"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"}
|
package/dist/ws/turns.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const authorizationContextBrand = Symbol("AuthorizationContext");
|
|
1
2
|
/**
|
|
2
3
|
* Bytes one queued entry keeps alive in this process until its turn runs.
|
|
3
4
|
*
|
|
@@ -39,10 +40,14 @@ export class TurnCoordinator {
|
|
|
39
40
|
running = new Set();
|
|
40
41
|
bySession = new Map();
|
|
41
42
|
startingSessions = 0;
|
|
43
|
+
/** Reserve a known session before asynchronous backend routing. */
|
|
44
|
+
startingBySession = new Map();
|
|
42
45
|
pendingApprovals = new Map();
|
|
43
46
|
pendingAskUser = new Map();
|
|
44
47
|
pendingLocation = new Map();
|
|
45
48
|
pendingMask = new Map();
|
|
49
|
+
/** Every authorization context with at least one live owner or async lease. */
|
|
50
|
+
authorizationRegistry = new Map();
|
|
46
51
|
locationCounter = 0;
|
|
47
52
|
maskCounter = 0;
|
|
48
53
|
nextLocationRequestId() {
|
|
@@ -55,9 +60,88 @@ export class TurnCoordinator {
|
|
|
55
60
|
isTurnActive() {
|
|
56
61
|
return this.running.size > 0;
|
|
57
62
|
}
|
|
63
|
+
/** Construct and synchronously register a context with its initial reference. */
|
|
64
|
+
openAuthorization(input) {
|
|
65
|
+
let initialReleased = false;
|
|
66
|
+
const releaseReference = () => {
|
|
67
|
+
const count = this.authorizationRegistry.get(authorization);
|
|
68
|
+
if (count === undefined)
|
|
69
|
+
return;
|
|
70
|
+
if (count === 1)
|
|
71
|
+
this.authorizationRegistry.delete(authorization);
|
|
72
|
+
else
|
|
73
|
+
this.authorizationRegistry.set(authorization, count - 1);
|
|
74
|
+
};
|
|
75
|
+
const authorization = {
|
|
76
|
+
[authorizationContextBrand]: true,
|
|
77
|
+
...input,
|
|
78
|
+
retain: () => {
|
|
79
|
+
const count = this.authorizationRegistry.get(authorization);
|
|
80
|
+
if (count === undefined) {
|
|
81
|
+
throw new Error("Cannot retain a released authorization context");
|
|
82
|
+
}
|
|
83
|
+
this.authorizationRegistry.set(authorization, count + 1);
|
|
84
|
+
let released = false;
|
|
85
|
+
return () => {
|
|
86
|
+
if (released)
|
|
87
|
+
return;
|
|
88
|
+
released = true;
|
|
89
|
+
releaseReference();
|
|
90
|
+
};
|
|
91
|
+
},
|
|
92
|
+
release: () => {
|
|
93
|
+
if (initialReleased)
|
|
94
|
+
return;
|
|
95
|
+
initialReleased = true;
|
|
96
|
+
releaseReference();
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
this.authorizationRegistry.set(authorization, 1);
|
|
100
|
+
return authorization;
|
|
101
|
+
}
|
|
102
|
+
/** Add principals whose registered authorization context has expired. */
|
|
103
|
+
collectExpiredPrincipalIds(now, ids) {
|
|
104
|
+
for (const authorization of this.authorizationRegistry.keys()) {
|
|
105
|
+
if (authorization.valid && authorization.expiresAt <= now) {
|
|
106
|
+
ids.add(authorization.principalId);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/** Invalidate every registered context belonging to one of these principals. */
|
|
111
|
+
invalidateAuthorizations(principalIds) {
|
|
112
|
+
for (const authorization of this.authorizationRegistry.keys()) {
|
|
113
|
+
if (principalIds.has(authorization.principalId))
|
|
114
|
+
authorization.valid = false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Record revocation without aborting running work. Unstarted queued work is
|
|
119
|
+
* removed per sender, while a current turn merely carries the invalid marker.
|
|
120
|
+
*/
|
|
121
|
+
revokePrincipals(principalIds) {
|
|
122
|
+
const affectedRunning = [];
|
|
123
|
+
this.invalidateAuthorizations(principalIds);
|
|
124
|
+
for (const turn of this.running) {
|
|
125
|
+
if (principalIds.has(turn.principalId)) {
|
|
126
|
+
affectedRunning.push(turn);
|
|
127
|
+
}
|
|
128
|
+
turn.queue = turn.queue.filter((entry) => {
|
|
129
|
+
if (!principalIds.has(entry.principalId))
|
|
130
|
+
return true;
|
|
131
|
+
entry.releaseAuthorization();
|
|
132
|
+
return false;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
return affectedRunning;
|
|
136
|
+
}
|
|
58
137
|
/** Cancel every running turn (used on shutdown). */
|
|
59
138
|
cancelAll(reason) {
|
|
60
|
-
|
|
139
|
+
const hadWork = this.running.size > 0 || this.startingBySession.size > 0;
|
|
140
|
+
for (const starting of this.startingBySession.values()) {
|
|
141
|
+
starting.cancelled = true;
|
|
142
|
+
starting.queue.length = 0;
|
|
143
|
+
}
|
|
144
|
+
if (!hadWork)
|
|
61
145
|
return false;
|
|
62
146
|
for (const turn of [...this.running])
|
|
63
147
|
this.cancelTurn(turn, reason);
|
|
@@ -66,11 +150,19 @@ export class TurnCoordinator {
|
|
|
66
150
|
/** Cancel a session's current turn and drop its queued follow-ups. */
|
|
67
151
|
cancelTurn(turn, reason) {
|
|
68
152
|
turn.cancelled = true;
|
|
69
|
-
turn.queue.
|
|
153
|
+
for (const entry of turn.queue.splice(0))
|
|
154
|
+
entry.releaseAuthorization();
|
|
70
155
|
clearTimeout(turn.timeoutHandle);
|
|
71
156
|
turn.abortController.abort();
|
|
72
157
|
this.drainPendingForTurn(turn, reason);
|
|
73
158
|
}
|
|
159
|
+
/** Record now, or retain the actor across the pre-recorder startup window. */
|
|
160
|
+
recordCancellation(turn, principalId) {
|
|
161
|
+
if (turn.recorder)
|
|
162
|
+
turn.recorder.recordCancellation(principalId);
|
|
163
|
+
else
|
|
164
|
+
turn.pendingCancellationPrincipalIds.push(principalId);
|
|
165
|
+
}
|
|
74
166
|
/** Resolve/reject every pending interactive request belonging to one turn. */
|
|
75
167
|
drainPendingForTurn(turn, reason) {
|
|
76
168
|
for (const [id, p] of this.pendingApprovals) {
|
|
@@ -136,7 +228,14 @@ export class TurnCoordinator {
|
|
|
136
228
|
for (const turn of [...this.running]) {
|
|
137
229
|
clearTimeout(turn.timeoutHandle);
|
|
138
230
|
turn.abortController.abort();
|
|
231
|
+
for (const entry of turn.queue.splice(0))
|
|
232
|
+
entry.releaseAuthorization();
|
|
233
|
+
}
|
|
234
|
+
for (const starting of this.startingBySession.values()) {
|
|
235
|
+
starting.cancelled = true;
|
|
236
|
+
starting.queue.length = 0;
|
|
139
237
|
}
|
|
238
|
+
this.startingBySession.clear();
|
|
140
239
|
this.running.clear();
|
|
141
240
|
this.bySession.clear();
|
|
142
241
|
this.pendingApprovals.clear();
|
package/dist/ws/turns.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"turns.js","sourceRoot":"","sources":["../../src/ws/turns.ts"],"names":[],"mappings":"AAeA,MAAM,yBAAyB,GAAkB,MAAM,CAAC,sBAAsB,CAAC,CAAC;AAyBhF;;;;;;;;;;GAUG;AACH,MAAM,UAAU,mBAAmB,CACjC,KAAmD;IAEnD,IAAI,KAAK,GAAG,MAAM,CAAC,UAAU,CAAC,KAAK,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;IACnD,KAAK,MAAM,UAAU,IAAI,KAAK,CAAC,WAAW,EAAE,CAAC;QAC3C,KAAK,IAAI,UAAU,CAAC,IAAI,CAAC,MAAM,CAAC;IAClC,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,mEAAmE;AACnE,MAAM,UAAU,WAAW,CAAC,IAAgC;IAC1D,IAAI,KAAK,GAAG,CAAC,CAAC;IACd,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK;QAAE,KAAK,IAAI,mBAAmB,CAAC,KAAK,CAAC,CAAC;IACpE,OAAO,KAAK,CAAC;AACf,CAAC;AAqFD;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IAC1B;;;;OAIG;IACH,GAAG,CAAU;IAEJ,OAAO,GAAG,IAAI,GAAG,EAAe,CAAC;IACjC,SAAS,GAAG,IAAI,GAAG,EAAuB,CAAC;IACpD,gBAAgB,GAAG,CAAC,CAAC;IACrB,mEAAmE;IAC1D,iBAAiB,GAAG,IAAI,GAAG,EAA2D,CAAC;IAEvF,gBAAgB,GAAG,IAAI,GAAG,EAA2B,CAAC;IACtD,cAAc,GAAG,IAAI,GAAG,EAA0B,CAAC;IACnD,eAAe,GAAG,IAAI,GAAG,EAA2B,CAAC;IACrD,WAAW,GAAG,IAAI,GAAG,EAAuB,CAAC;IAEtD,+EAA+E;IACtE,qBAAqB,GAAG,IAAI,GAAG,EAAgC,CAAC;IAEjE,eAAe,GAAG,CAAC,CAAC;IACpB,WAAW,GAAG,CAAC,CAAC;IAExB,qBAAqB;QACnB,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,eAAe,EAAE,CAAC;IACpE,CAAC;IAED,iBAAiB;QACf,OAAO,QAAQ,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,IAAI,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;IACjE,CAAC;IAED,iDAAiD;IACjD,YAAY;QACV,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;IAC/B,CAAC;IAED,iFAAiF;IACjF,iBAAiB,CAAC,KAIjB;QACC,IAAI,eAAe,GAAG,KAAK,CAAC;QAC5B,MAAM,gBAAgB,GAAG,GAAG,EAAE;YAC5B,MAAM,KAAK,GAAG,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;YAC5D,IAAI,KAAK,KAAK,SAAS;gBAAE,OAAO;YAChC,IAAI,KAAK,KAAK,CAAC;gBAAE,IAAI,CAAC,qBAAqB,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC;;gBAC7D,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;QAChE,CAAC,CAAC;QACF,MAAM,aAAa,GAAyB;YAC1C,CAAC,yBAAyB,CAAC,EAAE,IAAI;YACjC,GAAG,KAAK;YACR,MAAM,EAAE,GAAG,EAAE;gBACX,MAAM,KAAK,GAAG,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;gBAC5D,IAAI,KAAK,KAAK,SAAS,EAAE,CAAC;oBACxB,MAAM,IAAI,KAAK,CAAC,gDAAgD,CAAC,CAAC;gBACpE,CAAC;gBACD,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,EAAE,KAAK,GAAG,CAAC,CAAC,CAAC;gBACzD,IAAI,QAAQ,GAAG,KAAK,CAAC;gBACrB,OAAO,GAAG,EAAE;oBACV,IAAI,QAAQ;wBAAE,OAAO;oBACrB,QAAQ,GAAG,IAAI,CAAC;oBAChB,gBAAgB,EAAE,CAAC;gBACrB,CAAC,CAAC;YACJ,CAAC;YACD,OAAO,EAAE,GAAG,EAAE;gBACZ,IAAI,eAAe;oBAAE,OAAO;gBAC5B,eAAe,GAAG,IAAI,CAAC;gBACvB,gBAAgB,EAAE,CAAC;YACrB,CAAC;SACF,CAAC;QACF,IAAI,CAAC,qBAAqB,CAAC,GAAG,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC;QACjD,OAAO,aAAa,CAAC;IACvB,CAAC;IAED,yEAAyE;IACzE,0BAA0B,CAAC,GAAW,EAAE,GAAgB;QACtD,KAAK,MAAM,aAAa,IAAI,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9D,IAAI,aAAa,CAAC,KAAK,IAAI,aAAa,CAAC,SAAS,IAAI,GAAG,EAAE,CAAC;gBAC1D,GAAG,CAAC,GAAG,CAAC,aAAa,CAAC,WAAW,CAAC,CAAC;YACrC,CAAC;QACH,CAAC;IACH,CAAC;IAED,gFAAgF;IAChF,wBAAwB,CAAC,YAAiC;QACxD,KAAK,MAAM,aAAa,IAAI,IAAI,CAAC,qBAAqB,CAAC,IAAI,EAAE,EAAE,CAAC;YAC9D,IAAI,YAAY,CAAC,GAAG,CAAC,aAAa,CAAC,WAAW,CAAC;gBAAE,aAAa,CAAC,KAAK,GAAG,KAAK,CAAC;QAC/E,CAAC;IACH,CAAC;IAED;;;OAGG;IACH,gBAAgB,CAAC,YAAiC;QAChD,MAAM,eAAe,GAAkB,EAAE,CAAC;QAC1C,IAAI,CAAC,wBAAwB,CAAC,YAAY,CAAC,CAAC;QAC5C,KAAK,MAAM,IAAI,IAAI,IAAI,CAAC,OAAO,EAAE,CAAC;YAChC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC,WAAW,CAAC,EAAE,CAAC;gBACvC,eAAe,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YAC7B,CAAC;YACD,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;gBACvC,IAAI,CAAC,YAAY,CAAC,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC;oBAAE,OAAO,IAAI,CAAC;gBACtD,KAAK,CAAC,oBAAoB,EAAE,CAAC;gBAC7B,OAAO,KAAK,CAAC;YACf,CAAC,CAAC,CAAC;QACL,CAAC;QACD,OAAO,eAAe,CAAC;IACzB,CAAC;IAED,oDAAoD;IACpD,SAAS,CAAC,MAAc;QACtB,MAAM,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,GAAG,CAAC,IAAI,IAAI,CAAC,iBAAiB,CAAC,IAAI,GAAG,CAAC,CAAC;QACzE,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;YACvD,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC;YAC1B,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,OAAO;YAAE,OAAO,KAAK,CAAC;QAC3B,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC;YAAE,IAAI,CAAC,UAAU,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;QACpE,OAAO,IAAI,CAAC;IACd,CAAC;IAED,sEAAsE;IACtE,UAAU,CAAC,IAAiB,EAAE,MAAc;QAC1C,IAAI,CAAC,SAAS,GAAG,IAAI,CAAC;QACtB,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAAE,KAAK,CAAC,oBAAoB,EAAE,CAAC;QACvE,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QACjC,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,mBAAmB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACzC,CAAC;IAED,8EAA8E;IAC9E,kBAAkB,CAAC,IAAiB,EAAE,WAAmB;QACvD,IAAI,IAAI,CAAC,QAAQ;YAAE,IAAI,CAAC,QAAQ,CAAC,kBAAkB,CAAC,WAAW,CAAC,CAAC;;YAC5D,IAAI,CAAC,+BAA+B,CAAC,IAAI,CAAC,WAAW,CAAC,CAAC;IAC9D,CAAC;IAED,8EAA8E;IAC9E,mBAAmB,CAAC,IAAiB,EAAE,MAAc;QACnD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,gBAAgB,EAAE,CAAC;YAC5C,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC9B,CAAC,CAAC,OAAO,CAAC,EAAE,QAAQ,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,CAAC;YACjD,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACnC,CAAC;QACD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,cAAc,EAAE,CAAC;YAC1C,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC9B,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,cAAc,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,CAAC,uBAAuB,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IAC7C,CAAC;IAED;;;;;;;;;OASG;IACH,uBAAuB,CAAC,IAAiB,EAAE,MAAc;QACvD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,eAAe,EAAE,CAAC;YAC3C,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC9B,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,eAAe,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAClC,CAAC;QACD,KAAK,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,IAAI,IAAI,CAAC,WAAW,EAAE,CAAC;YACvC,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI;gBAAE,SAAS;YAC9B,CAAC,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;YAC5B,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,EAAE,CAAC,CAAC;QAC9B,CAAC;IACH,CAAC;IAED;;;;;;;OAOG;IACH,mBAAmB,CACjB,GAAmB,EACnB,EAAU,EACV,IAAiB;QAEjB,MAAM,QAAQ,GAAG,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;QAC7B,IAAI,QAAQ,IAAI,QAAQ,CAAC,IAAI,KAAK,IAAI,EAAE,CAAC;YACvC,IAAI,CAAC,GAAG,EAAE,IAAI,CAAC;gBACb,YAAY,EAAE,OAAO;gBACrB,IAAI,EAAE,4DAA4D;gBAClE,UAAU,EAAE,EAAE,YAAY,EAAE,EAAE,EAAE;aACjC,CAAC,CAAC;YACH,OAAO,IAAI,CAAC;QACd,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;YACrC,YAAY,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;YACjC,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;YAC7B,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC;gBAAE,KAAK,CAAC,oBAAoB,EAAE,CAAC;QACzE,CAAC;QACD,KAAK,MAAM,QAAQ,IAAI,IAAI,CAAC,iBAAiB,CAAC,MAAM,EAAE,EAAE,CAAC;YACvD,QAAQ,CAAC,SAAS,GAAG,IAAI,CAAC;YAC1B,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC;QAC5B,CAAC;QACD,IAAI,CAAC,iBAAiB,CAAC,KAAK,EAAE,CAAC;QAC/B,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;QACrB,IAAI,CAAC,SAAS,CAAC,KAAK,EAAE,CAAC;QACvB,IAAI,CAAC,gBAAgB,CAAC,KAAK,EAAE,CAAC;QAC9B,IAAI,CAAC,cAAc,CAAC,KAAK,EAAE,CAAC;QAC5B,IAAI,CAAC,eAAe,CAAC,KAAK,EAAE,CAAC;QAC7B,IAAI,CAAC,gBAAgB,GAAG,CAAC,CAAC;IAC5B,CAAC;CACF"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
CREATE TABLE principals (
|
|
2
|
+
id TEXT PRIMARY KEY NOT NULL, -- 22 chars base64url, server-generated
|
|
3
|
+
kind TEXT NOT NULL CHECK (kind IN ('owner','agent','ambient','system')),
|
|
4
|
+
auth_method TEXT NOT NULL CHECK (auth_method IN ('password','passkey','delegated','ambient')),
|
|
5
|
+
label TEXT NOT NULL, -- ≤64 chars, never rendered as HTML
|
|
6
|
+
credential_id TEXT, -- passkey_credentials.id, else NULL
|
|
7
|
+
created_by TEXT, -- minting principal, for delegation
|
|
8
|
+
created_at INTEGER NOT NULL,
|
|
9
|
+
expires_at INTEGER NOT NULL,
|
|
10
|
+
last_seen_at INTEGER,
|
|
11
|
+
revoked_at INTEGER
|
|
12
|
+
);
|
|
13
|
+
CREATE INDEX idx_principals_live ON principals(revoked_at, expires_at);
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
-- Principal attribution for activity spans and durable run rollups.
|
|
2
|
+
--
|
|
3
|
+
-- Deliberately NO backfill and no owner default: activity recorded before
|
|
4
|
+
-- this migration remains NULL (= unattributed). Rollups snapshot the live
|
|
5
|
+
-- principal's label and kind when they are first written because principal
|
|
6
|
+
-- rows expire and are pruned while activity_run_rollups are retained.
|
|
7
|
+
ALTER TABLE activity_spans ADD COLUMN principal_id TEXT;
|
|
8
|
+
|
|
9
|
+
ALTER TABLE activity_run_rollups ADD COLUMN principal_id TEXT;
|
|
10
|
+
ALTER TABLE activity_run_rollups ADD COLUMN principal_label TEXT;
|
|
11
|
+
ALTER TABLE activity_run_rollups ADD COLUMN principal_kind TEXT
|
|
12
|
+
CHECK (principal_kind IN ('owner', 'agent', 'ambient', 'system'));
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
-- Legacy subscriptions deliberately remain NULL and therefore inert until an
|
|
2
|
+
-- authenticated client re-registers the endpoint for its current principal.
|
|
3
|
+
ALTER TABLE push_subscriptions
|
|
4
|
+
ADD COLUMN principal_id TEXT REFERENCES principals(id) ON DELETE SET NULL;
|
|
5
|
+
|
|
6
|
+
CREATE INDEX idx_push_subscriptions_principal
|
|
7
|
+
ON push_subscriptions(principal_id);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
-- Advance the legacy v1 cookie epoch once at upgrade so every pre-upgrade
|
|
2
|
+
-- cookie stays invalid if the deployment rolls back to the v1 verifier.
|
|
3
|
+
INSERT INTO settings (key, value, updated_at)
|
|
4
|
+
VALUES ('auth.sessionsEpoch', '1', CAST(strftime('%s', 'now') AS INTEGER) * 1000)
|
|
5
|
+
ON CONFLICT(key) DO UPDATE SET
|
|
6
|
+
value = CASE
|
|
7
|
+
WHEN json_valid(value) THEN CASE
|
|
8
|
+
WHEN json_type(value) = 'integer' AND CAST(value AS INTEGER) >= 0
|
|
9
|
+
THEN CAST(CAST(value AS INTEGER) + 1 AS TEXT)
|
|
10
|
+
ELSE '1'
|
|
11
|
+
END
|
|
12
|
+
ELSE '1'
|
|
13
|
+
END,
|
|
14
|
+
updated_at = excluded.updated_at;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@schlessera/brain-ui-server",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.0",
|
|
4
4
|
"description": "brain-kit chat-UI server: Hono app factory, WebSocket turn coordinator, auth (password/passkeys/tailscale/proxy), session catalog, and brain/files/voice routes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"keywords": [
|
|
@@ -54,8 +54,8 @@
|
|
|
54
54
|
"dependencies": {
|
|
55
55
|
"@opentelemetry/api": "^1.9.1",
|
|
56
56
|
"@opentelemetry/api-logs": "0.221.0",
|
|
57
|
-
"@schlessera/brain-render-template": "0.
|
|
58
|
-
"@schlessera/brain-ui-sdk": "0.
|
|
57
|
+
"@schlessera/brain-render-template": "0.35.0",
|
|
58
|
+
"@schlessera/brain-ui-sdk": "0.35.0",
|
|
59
59
|
"@simplewebauthn/server": "^13.3.2",
|
|
60
60
|
"fflate": "^0.8.2",
|
|
61
61
|
"gray-matter": "^4.0.3",
|
package/src/activity/notify.ts
CHANGED
|
@@ -23,7 +23,7 @@ import type { Database } from "bun:sqlite";
|
|
|
23
23
|
import type { Logger } from "@opentelemetry/api-logs";
|
|
24
24
|
import { isFailureOutcome } from "@schlessera/brain-ui-sdk/protocol";
|
|
25
25
|
|
|
26
|
-
import { getSetting } from "../db/settings.js";
|
|
26
|
+
import { getSetting, setSetting } from "../db/settings.js";
|
|
27
27
|
import type { ActivityStore, SpanRow } from "./store.js";
|
|
28
28
|
|
|
29
29
|
export type IntentKind = "failure" | "completion" | "stuck";
|
|
@@ -76,7 +76,7 @@ const DEFAULT_STUCK_THRESHOLD_MS = 45 * 60 * 1000;
|
|
|
76
76
|
|
|
77
77
|
export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNotifier {
|
|
78
78
|
const { db, store, isWatched, log } = deps;
|
|
79
|
-
|
|
79
|
+
const cursorKey = "activity.notify.cursor";
|
|
80
80
|
/** Runs already flagged as stuck this process lifetime (tag also guards). */
|
|
81
81
|
const stuckFlagged = new Set<string>();
|
|
82
82
|
|
|
@@ -97,6 +97,9 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
|
|
|
97
97
|
suppressed?: boolean;
|
|
98
98
|
}): void {
|
|
99
99
|
const now = Date.now();
|
|
100
|
+
// Replay on first upgrade must not resurrect an acknowledged notification.
|
|
101
|
+
if (db.query("SELECT id FROM notification_intents WHERE run_id = ? AND kind = ? LIMIT 1")
|
|
102
|
+
.get(input.runId, input.kind)) return;
|
|
100
103
|
// Tag dedupe: one live (unacknowledged) intent per tag — repeats coalesce.
|
|
101
104
|
const existing = db
|
|
102
105
|
.query(
|
|
@@ -180,11 +183,17 @@ export function createActivityNotifier(deps: ActivityNotifierDeps): ActivityNoti
|
|
|
180
183
|
// The head is read BEFORE the detection query so advancing past
|
|
181
184
|
// non-terminal changes can never skip a terminal write committed
|
|
182
185
|
// in between; a change seen twice is absorbed by the tag dedupe.
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
db.transaction(() => {
|
|
187
|
+
const saved = getSetting<unknown>(db, cursorKey, 0, log);
|
|
188
|
+
const cursor = typeof saved === "number" && Number.isSafeInteger(saved) && saved >= 0 ? saved : 0;
|
|
189
|
+
const head = latestCursor(db);
|
|
190
|
+
const rows = store.terminalRootChangesSince(cursor, 500);
|
|
191
|
+
for (const { span } of rows) intentForSpan(span);
|
|
192
|
+
const lastSeen = rows.length > 0 ? rows[rows.length - 1]!.changeId : cursor;
|
|
193
|
+
// Commit intents and progress together. A crash retries the entire
|
|
194
|
+
// batch; another notifier reads the committed cursor under this lock.
|
|
195
|
+
setSetting(db, cursorKey, rows.length === 500 ? lastSeen : Math.max(head, lastSeen));
|
|
196
|
+
}).immediate();
|
|
188
197
|
|
|
189
198
|
// Watchdog: an over-threshold LIVE root run is stuck — a signal, not
|
|
190
199
|
// an outcome. Threshold from settings, per-job override supported.
|
|
@@ -27,13 +27,21 @@ export interface PushSubscriptionRow {
|
|
|
27
27
|
label: string | null;
|
|
28
28
|
createdAt: number;
|
|
29
29
|
lastUsedAt: number | null;
|
|
30
|
+
principalId: string | null;
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
export interface PushSender {
|
|
33
34
|
publicKey(): string;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
35
|
+
/** Bind (or re-bind) an endpoint only while the registering principal is live. */
|
|
36
|
+
subscribe(
|
|
37
|
+
sub: { endpoint: string; keys: { p256dh: string; auth: string } },
|
|
38
|
+
principalId: string,
|
|
39
|
+
label?: string
|
|
40
|
+
): boolean;
|
|
41
|
+
unsubscribe(endpoint: string, principalId: string): boolean;
|
|
42
|
+
subscriptions(principalId?: string): PushSubscriptionRow[];
|
|
43
|
+
/** Make one revoked principal's endpoints inert without deleting browser state. */
|
|
44
|
+
unbindPrincipal(principalId: string): number;
|
|
37
45
|
/** Deliver pending intents to every subscription. Returns sends attempted. */
|
|
38
46
|
deliverPending(notifier: ActivityNotifier): Promise<number>;
|
|
39
47
|
}
|
|
@@ -86,9 +94,9 @@ export function createPushSender(
|
|
|
86
94
|
return vapid();
|
|
87
95
|
}
|
|
88
96
|
|
|
89
|
-
function
|
|
97
|
+
function mapRows(dbRows: any[]): PushSubscriptionRow[] {
|
|
90
98
|
return (
|
|
91
|
-
|
|
99
|
+
dbRows
|
|
92
100
|
).map((r) => ({
|
|
93
101
|
endpoint: r.endpoint,
|
|
94
102
|
p256dh: r.p256dh,
|
|
@@ -96,33 +104,88 @@ export function createPushSender(
|
|
|
96
104
|
label: r.label,
|
|
97
105
|
createdAt: r.created_at,
|
|
98
106
|
lastUsedAt: r.last_used_at,
|
|
107
|
+
principalId: r.principal_id,
|
|
99
108
|
}));
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
function rows(principalId?: string): PushSubscriptionRow[] {
|
|
112
|
+
const dbRows = principalId === undefined
|
|
113
|
+
? db.query("SELECT * FROM push_subscriptions ORDER BY created_at").all()
|
|
114
|
+
: db
|
|
115
|
+
.query(
|
|
116
|
+
`SELECT * FROM push_subscriptions
|
|
117
|
+
WHERE principal_id = ? ORDER BY created_at`
|
|
118
|
+
)
|
|
119
|
+
.all(principalId);
|
|
120
|
+
return mapRows(dbRows as any[]);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function deliverableRows(now: number): PushSubscriptionRow[] {
|
|
124
|
+
return mapRows(
|
|
125
|
+
db
|
|
126
|
+
.query(
|
|
127
|
+
`SELECT s.* FROM push_subscriptions s
|
|
128
|
+
JOIN principals p ON p.id = s.principal_id
|
|
129
|
+
WHERE p.revoked_at IS NULL
|
|
130
|
+
AND typeof(p.expires_at) = 'integer'
|
|
131
|
+
AND p.expires_at > ?
|
|
132
|
+
ORDER BY s.created_at`
|
|
133
|
+
)
|
|
134
|
+
.all(now) as any[]
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
102
138
|
return {
|
|
103
139
|
publicKey() {
|
|
104
140
|
return vapid().publicKey;
|
|
105
141
|
},
|
|
106
142
|
|
|
107
|
-
subscribe(sub, label) {
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
143
|
+
subscribe(sub, principalId, label) {
|
|
144
|
+
const now = Date.now();
|
|
145
|
+
return db.query(
|
|
146
|
+
`INSERT INTO push_subscriptions
|
|
147
|
+
(endpoint, p256dh, auth, label, created_at, principal_id)
|
|
148
|
+
SELECT ?, ?, ?, ?, ?, id FROM principals
|
|
149
|
+
WHERE id = ? AND revoked_at IS NULL
|
|
150
|
+
AND typeof(expires_at) = 'integer' AND expires_at > ?
|
|
151
|
+
ON CONFLICT(endpoint) DO UPDATE SET
|
|
152
|
+
p256dh = excluded.p256dh,
|
|
153
|
+
auth = excluded.auth,
|
|
154
|
+
label = excluded.label,
|
|
155
|
+
principal_id = excluded.principal_id`
|
|
156
|
+
).run(
|
|
157
|
+
sub.endpoint,
|
|
158
|
+
sub.keys.p256dh,
|
|
159
|
+
sub.keys.auth,
|
|
160
|
+
label ?? null,
|
|
161
|
+
now,
|
|
162
|
+
principalId,
|
|
163
|
+
now
|
|
164
|
+
).changes > 0;
|
|
113
165
|
},
|
|
114
166
|
|
|
115
|
-
unsubscribe(endpoint) {
|
|
116
|
-
return db
|
|
117
|
-
.
|
|
167
|
+
unsubscribe(endpoint, principalId) {
|
|
168
|
+
return db
|
|
169
|
+
.query(
|
|
170
|
+
"DELETE FROM push_subscriptions WHERE endpoint = ? AND principal_id = ?"
|
|
171
|
+
)
|
|
172
|
+
.run(endpoint, principalId).changes > 0;
|
|
118
173
|
},
|
|
119
174
|
|
|
120
175
|
subscriptions: rows,
|
|
121
176
|
|
|
177
|
+
unbindPrincipal(principalId) {
|
|
178
|
+
return db
|
|
179
|
+
.query(
|
|
180
|
+
"UPDATE push_subscriptions SET principal_id = NULL WHERE principal_id = ?"
|
|
181
|
+
)
|
|
182
|
+
.run(principalId).changes;
|
|
183
|
+
},
|
|
184
|
+
|
|
122
185
|
async deliverPending(notifier) {
|
|
123
186
|
const pending = notifier.pending();
|
|
124
187
|
if (pending.length === 0) return 0;
|
|
125
|
-
if (
|
|
188
|
+
if (deliverableRows(Date.now()).length === 0) {
|
|
126
189
|
// No devices: the inbox already has it; nothing to deliver. Leave
|
|
127
190
|
// the intents pending so a device subscribing later still gets them
|
|
128
191
|
// if they are fresh, and the boot sweep can retry.
|
|
@@ -134,7 +197,7 @@ export function createPushSender(
|
|
|
134
197
|
for (const intent of pending) {
|
|
135
198
|
// Re-read per intent: an endpoint pruned as dead during the previous
|
|
136
199
|
// intent's pass must not be attempted again in this one.
|
|
137
|
-
const subs =
|
|
200
|
+
const subs = deliverableRows(Date.now());
|
|
138
201
|
if (subs.length === 0) break;
|
|
139
202
|
// Minimized payload; the tag coalesces repeats per run across devices.
|
|
140
203
|
const payload = JSON.stringify({
|
package/src/activity/query.ts
CHANGED
|
@@ -50,6 +50,9 @@ export function runActivityQuery(
|
|
|
50
50
|
name: r.name,
|
|
51
51
|
jobName: r.jobName,
|
|
52
52
|
sessionId: r.sessionId,
|
|
53
|
+
principalId: r.principalId,
|
|
54
|
+
principalLabel: r.principalLabel,
|
|
55
|
+
principalKind: r.principalKind,
|
|
53
56
|
startedAt: iso(r.startedAt),
|
|
54
57
|
outcome: r.outcome,
|
|
55
58
|
durationMs: r.durationMs,
|
|
@@ -89,6 +92,7 @@ export function runActivityQuery(
|
|
|
89
92
|
parent: s.parentSpanId,
|
|
90
93
|
name: s.name,
|
|
91
94
|
kind: s.kind,
|
|
95
|
+
principalId: s.principalId,
|
|
92
96
|
startedAt: iso(s.startedAt),
|
|
93
97
|
endedAt: s.endedAt ? iso(s.endedAt) : null,
|
|
94
98
|
outcome: s.outcome,
|
|
@@ -112,6 +116,9 @@ export function runActivityQuery(
|
|
|
112
116
|
rollup: {
|
|
113
117
|
name: rollup.name,
|
|
114
118
|
jobName: rollup.jobName,
|
|
119
|
+
principalId: rollup.principalId,
|
|
120
|
+
principalLabel: rollup.principalLabel,
|
|
121
|
+
principalKind: rollup.principalKind,
|
|
115
122
|
outcome: rollup.outcome,
|
|
116
123
|
startedAt: iso(rollup.startedAt),
|
|
117
124
|
durationMs: rollup.durationMs,
|
|
@@ -173,6 +180,7 @@ function liveSummary(span: SpanRow) {
|
|
|
173
180
|
name: span.name,
|
|
174
181
|
jobName: span.jobName,
|
|
175
182
|
sessionId: span.sessionId,
|
|
183
|
+
principalId: span.principalId,
|
|
176
184
|
startedAt: iso(span.startedAt),
|
|
177
185
|
elapsedMs: Date.now() - span.startedAt,
|
|
178
186
|
};
|