agl 40.5.1 → 41.0.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/changelog.md +43 -0
- package/dist_serve/bundle.js +3514 -3357
- package/dist_serve/bundle.js.map +2 -2
- package/dist_serve/bundle.js.third-party-notices.json +58 -32
- package/dist_serve/bundle.js.third-party-notices.txt +108 -33
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/aglapi/aglapisurface.document.js +64 -37
- package/dist_ts/classes.aglhome.d.ts +5 -0
- package/dist_ts/classes.aglhome.js +2 -1
- package/dist_ts/classes.authstore.js +5 -1
- package/dist_ts/classes.controller.d.ts +8 -0
- package/dist_ts/classes.controller.js +119 -22
- package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
- package/dist_ts/classes.controllereventdelivery.js +233 -0
- package/dist_ts/classes.flexmodels.public.js +4 -1
- package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
- package/dist_ts/classes.flexprojectionreader.js +29 -5
- package/dist_ts/classes.flexservice.d.ts +2 -1
- package/dist_ts/classes.flexservice.js +34 -6
- package/dist_ts/classes.flexstore.d.ts +41 -4
- package/dist_ts/classes.flexstore.js +266 -203
- package/dist_ts/classes.inferencegateway.d.ts +35 -1
- package/dist_ts/classes.inferencegateway.js +86 -26
- package/dist_ts/classes.inferenceservice.d.ts +4 -2
- package/dist_ts/classes.inferenceservice.js +5 -4
- package/dist_ts/classes.proxycontrol.d.ts +58 -4
- package/dist_ts/classes.proxycontrol.js +23 -44
- package/dist_ts/classes.proxycontrolclient.d.ts +15 -5
- package/dist_ts/classes.proxycontrolclient.js +67 -17
- package/dist_ts/classes.proxydaemon.d.ts +22 -3
- package/dist_ts/classes.proxydaemon.js +73 -12
- package/dist_ts/classes.proxyleaseholder.d.ts +43 -14
- package/dist_ts/classes.proxyleaseholder.js +66 -22
- package/dist_ts/classes.proxystore.d.ts +9 -0
- package/dist_ts/classes.proxystore.js +15 -7
- package/dist_ts/classes.ticketnotifier.js +4 -2
- package/dist_ts/classes.ticketstore.d.ts +50 -2
- package/dist_ts/classes.ticketstore.js +195 -78
- package/dist_ts/classes.workercallerbindings.d.ts +66 -0
- package/dist_ts/classes.workercallerbindings.js +211 -0
- package/dist_ts/classes.workercallers.d.ts +63 -2
- package/dist_ts/classes.workercallers.js +430 -65
- package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
- package/dist_ts/driver/classes.drivereventrelay.js +99 -6
- package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
- package/dist_ts/driver/functions.driverinstructions.js +1 -1
- package/dist_ts/driver/functions.drivertools.d.ts +11 -2
- package/dist_ts/driver/functions.drivertools.js +19 -9
- package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
- package/dist_ts/fleet/classes.boardpump.js +38 -20
- package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
- package/dist_ts/fleet/classes.ticketeffects.js +16 -10
- package/dist_ts/fleet/classes.workerlaunches.d.ts +12 -5
- package/dist_ts/fleet/classes.workerlaunches.js +25 -19
- package/dist_ts/fleet/classes.workers.d.ts +21 -0
- package/dist_ts/fleet/classes.workers.js +110 -17
- package/dist_ts/fleet/functions.workervalidation.js +2 -2
- package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
- package/dist_ts/functions.flexprivatesnapshot.js +461 -0
- package/dist_ts/functions.inferencecli.d.ts +3 -2
- package/dist_ts/functions.inferencecli.js +67 -5
- package/dist_ts/functions.inferencecliarguments.d.ts +6 -2
- package/dist_ts/functions.inferencecliarguments.js +29 -2
- package/dist_ts/functions.inferencelaunch.d.ts +24 -0
- package/dist_ts/functions.inferencelaunch.js +42 -2
- package/dist_ts/functions.mcptickettools.d.ts +3 -0
- package/dist_ts/functions.mcptickettools.js +2 -2
- package/dist_ts/functions.proxycontract.d.ts +11 -0
- package/dist_ts/functions.proxycontract.js +12 -1
- package/dist_ts/functions.proxysocket.d.ts +6 -0
- package/dist_ts/functions.proxysocket.js +46 -0
- package/dist_ts/functions.ticketcli.d.ts +1 -1
- package/dist_ts/functions.ticketcli.js +8 -4
- package/dist_ts/functions.ticketvalidation.d.ts +3 -0
- package/dist_ts/functions.ticketvalidation.js +7 -4
- package/dist_ts/functions.workerprocessproof.d.ts +6 -0
- package/dist_ts/functions.workerprocessproof.js +28 -4
- package/dist_ts/interfaces.flexipc.d.ts +21 -2
- package/dist_ts/interfaces.flexipc.js +33 -16
- package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
- package/dist_ts_interfaces/interfaces.d.ts +1 -1
- package/dist_ts_interfaces/interfaces.js +1 -1
- package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
- package/dist_ts_interfaces/tickets.d.ts +37 -9
- package/dist_ts_interfaces/tickets.js +7 -1
- package/dist_ts_interfaces/workercallers.d.ts +43 -4
- package/package.json +7 -7
- package/readme.md +156 -23
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/aglapi/aglapisurface.document.ts +63 -36
- package/ts/classes.aglhome.ts +6 -0
- package/ts/classes.authstore.ts +4 -0
- package/ts/classes.controller.ts +141 -32
- package/ts/classes.controllereventdelivery.ts +289 -0
- package/ts/classes.flexmodels.public.ts +10 -0
- package/ts/classes.flexprojectionreader.ts +30 -3
- package/ts/classes.flexservice.ts +33 -5
- package/ts/classes.flexstore.ts +381 -246
- package/ts/classes.inferencegateway.ts +104 -23
- package/ts/classes.inferenceservice.ts +6 -3
- package/ts/classes.proxycontrol.ts +76 -39
- package/ts/classes.proxycontrolclient.ts +72 -20
- package/ts/classes.proxydaemon.ts +85 -15
- package/ts/classes.proxyleaseholder.ts +84 -28
- package/ts/classes.proxystore.ts +16 -5
- package/ts/classes.ticketnotifier.ts +3 -1
- package/ts/classes.ticketstore.ts +201 -75
- package/ts/classes.workercallerbindings.ts +147 -0
- package/ts/classes.workercallers.ts +410 -59
- package/ts/driver/classes.drivereventrelay.ts +89 -4
- package/ts/driver/functions.driverinstructions.ts +1 -1
- package/ts/driver/functions.drivertools.ts +22 -8
- package/ts/fleet/classes.boardpump.ts +51 -19
- package/ts/fleet/classes.ticketeffects.ts +25 -11
- package/ts/fleet/classes.workerlaunches.ts +23 -15
- package/ts/fleet/classes.workers.ts +79 -17
- package/ts/fleet/functions.workervalidation.ts +1 -1
- package/ts/functions.flexprivatesnapshot.ts +601 -0
- package/ts/functions.inferencecli.ts +61 -7
- package/ts/functions.inferencecliarguments.ts +32 -1
- package/ts/functions.inferencelaunch.ts +49 -2
- package/ts/functions.mcptickettools.ts +1 -1
- package/ts/functions.proxycontract.ts +12 -0
- package/ts/functions.proxysocket.ts +39 -0
- package/ts/functions.ticketcli.ts +7 -3
- package/ts/functions.ticketvalidation.ts +7 -3
- package/ts/functions.workerprocessproof.ts +27 -4
- package/ts/interfaces.flexipc.ts +50 -21
- package/ts_interfaces/fleetworkers.ts +6 -0
- package/ts_interfaces/interfaces.ts +1 -1
- package/ts_interfaces/requests.tickets.ts +9 -6
- package/ts_interfaces/tickets.ts +32 -1
- package/ts_interfaces/workercallers.ts +32 -4
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/elements.harnesscontrollerapp.ts +19 -8
- package/ts_web/elements.ticketboard.ts +16 -4
- package/ts_web/settings.general.ts +10 -4
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import type { IControllerEvent } from '../ts_interfaces/interfaces.js';
|
|
2
|
+
|
|
3
|
+
/** One authenticated browser connection that receives controller events. */
|
|
4
|
+
export interface IControllerEventViewer {
|
|
5
|
+
peerId: string;
|
|
6
|
+
/** Sends one event; resolves on the browser's acknowledgement, rejects on its timeout or a closed connection. */
|
|
7
|
+
send(eventArg: IControllerEvent): Promise<void>;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface IControllerEventDeliveryLimits {
|
|
11
|
+
/** Events one viewer may have waiting before its queue collapses into a resync. */
|
|
12
|
+
events: number;
|
|
13
|
+
/** Serialized bytes one viewer may have waiting before its queue collapses into a resync. */
|
|
14
|
+
bytes: number;
|
|
15
|
+
/** The largest live message delta a merge in a viewer's queue may produce. */
|
|
16
|
+
mergedDeltaBytes: number;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface IControllerEventDeliveryOptions {
|
|
20
|
+
/** The connections that receive events now; read once per batch of pushed events. */
|
|
21
|
+
viewers(): Promise<IControllerEventViewer[]>;
|
|
22
|
+
/** Events with one key carry the same state, so in a collapsed queue the newest replaces the older ones. */
|
|
23
|
+
eventKey(eventArg: IControllerEvent): string;
|
|
24
|
+
/** Merges a contiguous live message delta into the delta queued just before it, or refuses with `undefined`. */
|
|
25
|
+
mergeDelta(previousArg: IControllerEvent, eventArg: IControllerEvent): IControllerEvent | undefined;
|
|
26
|
+
limits: IControllerEventDeliveryLimits;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface IQueuedControllerEvent {
|
|
30
|
+
event: IControllerEvent;
|
|
31
|
+
/** `JSON.stringify(event)` bytes; for a merged delta the sum of its parts, never less than its size. */
|
|
32
|
+
bytes: number;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface IViewerQueue {
|
|
36
|
+
viewer: IControllerEventViewer;
|
|
37
|
+
/** Waiting events in send order. Normal keys are a sequence; a collapsed queue keys by state. */
|
|
38
|
+
events: Map<string, IQueuedControllerEvent>;
|
|
39
|
+
/** The key of the newest waiting event while the queue is not collapsed: where a delta may merge. */
|
|
40
|
+
tailKey?: string;
|
|
41
|
+
bytes: number;
|
|
42
|
+
/** Set when the queue overflowed and until it drains: events then replace older ones of their state. */
|
|
43
|
+
collapsed: boolean;
|
|
44
|
+
sending?: Promise<void>;
|
|
45
|
+
removed: boolean;
|
|
46
|
+
/** How many times this queue collapsed. */
|
|
47
|
+
collapses: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface IPendingFanOut {
|
|
51
|
+
item: IQueuedControllerEvent;
|
|
52
|
+
delivered: () => void;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const isLiveEvent = (eventArg: IControllerEvent): boolean => Boolean(
|
|
56
|
+
eventArg.toolExecution
|
|
57
|
+
|| eventArg.reasoningUpdate
|
|
58
|
+
|| eventArg.reasoningDelta
|
|
59
|
+
|| eventArg.textUpdate
|
|
60
|
+
|| eventArg.textDelta,
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
/** The live stream content in one event, which only a delta's own successor or a re-read can replace. */
|
|
64
|
+
const isMessageDelta = (eventArg: IControllerEvent): boolean => Boolean(eventArg.reasoningDelta || eventArg.textDelta);
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Delivers controller events to every viewer through its own bounded, ordered queue, so a viewer that acknowledges
|
|
68
|
+
* slowly or never delays only itself. Each viewer has at most one event in flight and receives events in push order.
|
|
69
|
+
*
|
|
70
|
+
* A viewer whose queue passes its limits is resynchronized alone, with the existing Flex queue's collapse rules:
|
|
71
|
+
* the queue keeps the newest event of each state key (statuses, barriers, feed and ticket revisions, settings),
|
|
72
|
+
* drops live tool, reasoning and text content, and ends with `sessions.changed` for each harness whose live content
|
|
73
|
+
* it dropped, which makes the browser re-read the conversations and the open session (live snapshots included).
|
|
74
|
+
* The viewer's own revision checks catch any delta that no longer fits. Should a collapsed queue still hold more
|
|
75
|
+
* than its event limit, each session's states fold into their harness's `sessions.changed` too, so what remains is
|
|
76
|
+
* one event per harness and per project or global state. The queue stays collapsed until it drains.
|
|
77
|
+
*/
|
|
78
|
+
export class ControllerEventDelivery {
|
|
79
|
+
private readonly queues = new Map<string, IViewerQueue>();
|
|
80
|
+
private readonly fanOutQueue: IPendingFanOut[] = [];
|
|
81
|
+
private fanOutTask?: Promise<void>;
|
|
82
|
+
private sequence = 0;
|
|
83
|
+
private generation = 0;
|
|
84
|
+
|
|
85
|
+
constructor(private readonly options: IControllerEventDeliveryOptions) {}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Queues one event for every current viewer, in push order. Resolves once each viewer's queue holds it; it never
|
|
89
|
+
* waits for an acknowledgement.
|
|
90
|
+
*/
|
|
91
|
+
public push(eventArg: IControllerEvent): Promise<void> {
|
|
92
|
+
const item: IQueuedControllerEvent = {
|
|
93
|
+
event: eventArg,
|
|
94
|
+
bytes: Buffer.byteLength(JSON.stringify(eventArg), 'utf8'),
|
|
95
|
+
};
|
|
96
|
+
return new Promise<void>((resolve) => {
|
|
97
|
+
this.fanOutQueue.push({ item, delivered: resolve });
|
|
98
|
+
this.startFanOut();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** The events waiting for one viewer, in send order (the one in flight excluded). */
|
|
103
|
+
public waiting(peerIdArg: string): IControllerEvent[] {
|
|
104
|
+
return [...(this.queues.get(peerIdArg)?.events.values() ?? [])].map((itemArg) => itemArg.event);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** How many times the queue of a connected viewer collapsed into a resync. */
|
|
108
|
+
public collapseCount(peerIdArg: string): number {
|
|
109
|
+
return this.queues.get(peerIdArg)?.collapses ?? 0;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Drops every waiting event and waits for the sends in flight; later pushes start fresh queues. */
|
|
113
|
+
public async stop(): Promise<void> {
|
|
114
|
+
this.generation += 1;
|
|
115
|
+
const sending: Promise<void>[] = [];
|
|
116
|
+
for (const queue of this.queues.values()) {
|
|
117
|
+
queue.removed = true;
|
|
118
|
+
queue.events.clear();
|
|
119
|
+
queue.bytes = 0;
|
|
120
|
+
if (queue.sending) sending.push(queue.sending);
|
|
121
|
+
}
|
|
122
|
+
this.queues.clear();
|
|
123
|
+
if (this.fanOutTask) sending.push(this.fanOutTask);
|
|
124
|
+
await Promise.allSettled(sending);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private startFanOut(): void {
|
|
128
|
+
if (this.fanOutTask) return;
|
|
129
|
+
const task = this.fanOut().finally(() => {
|
|
130
|
+
if (this.fanOutTask === task) this.fanOutTask = undefined;
|
|
131
|
+
if (this.fanOutQueue.length > 0) this.startFanOut();
|
|
132
|
+
});
|
|
133
|
+
this.fanOutTask = task;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private async fanOut(): Promise<void> {
|
|
137
|
+
while (this.fanOutQueue.length > 0) {
|
|
138
|
+
const batch = this.fanOutQueue.splice(0);
|
|
139
|
+
const generation = this.generation;
|
|
140
|
+
let viewers: IControllerEventViewer[] = [];
|
|
141
|
+
try {
|
|
142
|
+
viewers = await this.options.viewers();
|
|
143
|
+
} catch {
|
|
144
|
+
// No viewer can be named now; the events reach none, as a push without connections.
|
|
145
|
+
}
|
|
146
|
+
if (generation === this.generation) {
|
|
147
|
+
const current = new Set(viewers.map((viewerArg) => viewerArg.peerId));
|
|
148
|
+
for (const [peerId, queue] of this.queues) {
|
|
149
|
+
if (current.has(peerId)) continue;
|
|
150
|
+
queue.removed = true;
|
|
151
|
+
queue.events.clear();
|
|
152
|
+
queue.bytes = 0;
|
|
153
|
+
this.queues.delete(peerId);
|
|
154
|
+
}
|
|
155
|
+
for (const viewer of viewers) {
|
|
156
|
+
let queue = this.queues.get(viewer.peerId);
|
|
157
|
+
if (!queue) {
|
|
158
|
+
queue = { viewer, events: new Map(), bytes: 0, collapsed: false, removed: false, collapses: 0 };
|
|
159
|
+
this.queues.set(viewer.peerId, queue);
|
|
160
|
+
}
|
|
161
|
+
queue.viewer = viewer;
|
|
162
|
+
for (const pending of batch) this.enqueue(queue, pending.item);
|
|
163
|
+
this.startSending(queue);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const pending of batch) pending.delivered();
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private enqueue(queueArg: IViewerQueue, itemArg: IQueuedControllerEvent): void {
|
|
171
|
+
if (queueArg.collapsed) {
|
|
172
|
+
this.absorb(queueArg, itemArg);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
if (isMessageDelta(itemArg.event) && queueArg.tailKey !== undefined) {
|
|
176
|
+
const tailKey = queueArg.tailKey;
|
|
177
|
+
const tail = queueArg.events.get(tailKey);
|
|
178
|
+
if (
|
|
179
|
+
tail !== undefined
|
|
180
|
+
&& isMessageDelta(tail.event)
|
|
181
|
+
&& tail.bytes + itemArg.bytes <= this.options.limits.mergedDeltaBytes
|
|
182
|
+
) {
|
|
183
|
+
const merged = this.options.mergeDelta(tail.event, itemArg.event);
|
|
184
|
+
if (merged) {
|
|
185
|
+
queueArg.events.set(tailKey, { event: merged, bytes: tail.bytes + itemArg.bytes });
|
|
186
|
+
queueArg.bytes += itemArg.bytes;
|
|
187
|
+
if (queueArg.bytes > this.options.limits.bytes) this.collapse(queueArg);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (
|
|
193
|
+
queueArg.events.size + 1 > this.options.limits.events
|
|
194
|
+
|| queueArg.bytes + itemArg.bytes > this.options.limits.bytes
|
|
195
|
+
) {
|
|
196
|
+
this.collapse(queueArg);
|
|
197
|
+
this.absorb(queueArg, itemArg);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.sequence += 1;
|
|
201
|
+
queueArg.tailKey = `#${this.sequence}`;
|
|
202
|
+
queueArg.events.set(queueArg.tailKey, itemArg);
|
|
203
|
+
queueArg.bytes += itemArg.bytes;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
private collapse(queueArg: IViewerQueue): void {
|
|
207
|
+
const dropped = [...queueArg.events.values()];
|
|
208
|
+
queueArg.events.clear();
|
|
209
|
+
queueArg.tailKey = undefined;
|
|
210
|
+
queueArg.bytes = 0;
|
|
211
|
+
queueArg.collapsed = true;
|
|
212
|
+
queueArg.collapses += 1;
|
|
213
|
+
for (const item of dropped) this.absorb(queueArg, item);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/** Adds an event to a collapsed queue: the newest of its state, or a resync of its harness for live content. */
|
|
217
|
+
private absorb(queueArg: IViewerQueue, itemArg: IQueuedControllerEvent): void {
|
|
218
|
+
if (isLiveEvent(itemArg.event)) {
|
|
219
|
+
this.resyncHarness(queueArg, itemArg.event);
|
|
220
|
+
} else {
|
|
221
|
+
this.place(queueArg, `state\0${this.options.eventKey(itemArg.event)}`, itemArg);
|
|
222
|
+
}
|
|
223
|
+
if (queueArg.events.size > this.options.limits.events) this.foldSessions(queueArg);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* A collapsed queue that still holds more states than its limit keeps only what is not about one session: each
|
|
228
|
+
* session's states become their harness's `sessions.changed`, whose re-read covers the open session and the list.
|
|
229
|
+
*/
|
|
230
|
+
private foldSessions(queueArg: IViewerQueue): void {
|
|
231
|
+
for (const [key, item] of [...queueArg.events]) {
|
|
232
|
+
if (!key.startsWith('state\0') || item.event.sessionId === undefined) continue;
|
|
233
|
+
queueArg.events.delete(key);
|
|
234
|
+
queueArg.bytes -= item.bytes;
|
|
235
|
+
this.resyncHarness(queueArg, item.event);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private resyncHarness(queueArg: IViewerQueue, eventArg: IControllerEvent): void {
|
|
240
|
+
const harnessId = eventArg.harnessId;
|
|
241
|
+
const event: IControllerEvent = {
|
|
242
|
+
type: 'sessions.changed',
|
|
243
|
+
...(harnessId === undefined ? {} : { harnessId }),
|
|
244
|
+
timestamp: eventArg.timestamp,
|
|
245
|
+
};
|
|
246
|
+
this.place(
|
|
247
|
+
queueArg,
|
|
248
|
+
`resync\0${harnessId ?? ''}`,
|
|
249
|
+
{ event, bytes: Buffer.byteLength(JSON.stringify(event), 'utf8') },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Puts an event last under its key, replacing an older one with that key. */
|
|
254
|
+
private place(queueArg: IViewerQueue, keyArg: string, itemArg: IQueuedControllerEvent): void {
|
|
255
|
+
const replaced = queueArg.events.get(keyArg);
|
|
256
|
+
if (replaced) {
|
|
257
|
+
queueArg.events.delete(keyArg);
|
|
258
|
+
queueArg.bytes -= replaced.bytes;
|
|
259
|
+
}
|
|
260
|
+
queueArg.events.set(keyArg, itemArg);
|
|
261
|
+
queueArg.bytes += itemArg.bytes;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private startSending(queueArg: IViewerQueue): void {
|
|
265
|
+
if (queueArg.sending || queueArg.events.size === 0) return;
|
|
266
|
+
const task = this.send(queueArg).finally(() => {
|
|
267
|
+
if (queueArg.sending === task) queueArg.sending = undefined;
|
|
268
|
+
if (!queueArg.removed && queueArg.events.size > 0) this.startSending(queueArg);
|
|
269
|
+
});
|
|
270
|
+
queueArg.sending = task;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
private async send(queueArg: IViewerQueue): Promise<void> {
|
|
274
|
+
while (!queueArg.removed) {
|
|
275
|
+
const next = queueArg.events.entries().next().value as [string, IQueuedControllerEvent] | undefined;
|
|
276
|
+
if (!next) return;
|
|
277
|
+
queueArg.events.delete(next[0]);
|
|
278
|
+
queueArg.bytes -= next[1].bytes;
|
|
279
|
+
if (queueArg.tailKey === next[0]) queueArg.tailKey = undefined;
|
|
280
|
+
if (queueArg.events.size === 0) queueArg.collapsed = false;
|
|
281
|
+
try {
|
|
282
|
+
await queueArg.viewer.send(next[1].event);
|
|
283
|
+
} catch {
|
|
284
|
+
// As before: a viewer that does not acknowledge in time misses nothing it already received, and a closed
|
|
285
|
+
// connection leaves the viewer set at the next push.
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
@@ -727,6 +727,16 @@ export class FlexPublicHeadModel extends plugins.nosqldb.SmartDataDbDoc<
|
|
|
727
727
|
name: 'flex_public_session_scope_purge',
|
|
728
728
|
key: { controllerId: 1, scopeId: 1, id: 1 },
|
|
729
729
|
})
|
|
730
|
+
/**
|
|
731
|
+
* One session's records in a scope: a reader finds the record a publication lists for a session
|
|
732
|
+
* among that session's versions, never among every session's records. Created by the startup
|
|
733
|
+
* init of the models (`initFlexPublicModels`, the projection reader's `init`), outside any
|
|
734
|
+
* transaction. An older AGL leaves the index it does not declare alone.
|
|
735
|
+
*/
|
|
736
|
+
@plugins.nosqldb.compoundIndex({
|
|
737
|
+
name: 'flex_public_session_lookup',
|
|
738
|
+
key: { controllerId: 1, scopeId: 1, 'session.sessionId': 1 },
|
|
739
|
+
})
|
|
730
740
|
@plugins.nosqldb.managed({ collectionName: flexPublicSessionsCollectionName })
|
|
731
741
|
export class FlexPublicSessionRecordModel extends plugins.nosqldb.SmartDataDbDoc<
|
|
732
742
|
FlexPublicSessionRecordModel,
|
|
@@ -414,9 +414,7 @@ export class FlexProjectionReader {
|
|
|
414
414
|
signalArg?: AbortSignal,
|
|
415
415
|
): Promise<TFlexSession> {
|
|
416
416
|
const selection = await this.selectCandidate(scopeIdArg, undefined, signalArg);
|
|
417
|
-
const
|
|
418
|
-
const record = records.find((entry) => entry.session.sessionId === sessionIdArg);
|
|
419
|
-
if (!record) throw new FlexProjectionNotFoundError();
|
|
417
|
+
const record = await this.loadSessionRecord(selection, sessionIdArg, signalArg);
|
|
420
418
|
return JSON.parse(JSON.stringify(record.session)) as TFlexSession;
|
|
421
419
|
}
|
|
422
420
|
|
|
@@ -627,6 +625,35 @@ export class FlexProjectionReader {
|
|
|
627
625
|
}).sort(compareNewestSession);
|
|
628
626
|
}
|
|
629
627
|
|
|
628
|
+
/**
|
|
629
|
+
* The record the selected publication lists for one session: looked up through the session
|
|
630
|
+
* index among the records the publication lists, so it reads that session's records only, and
|
|
631
|
+
* proven against its manifest.
|
|
632
|
+
*/
|
|
633
|
+
private async loadSessionRecord(
|
|
634
|
+
selectionArg: IFlexCandidateSelection,
|
|
635
|
+
sessionIdArg: string,
|
|
636
|
+
signalArg?: AbortSignal,
|
|
637
|
+
): Promise<IFlexPublicSessionRecordDocument> {
|
|
638
|
+
const sessionRecords = selectionArg.manifest.sessionRecords;
|
|
639
|
+
const models = await this.models.publicSessions.getOffsetPage({
|
|
640
|
+
controllerId: this.controllerId,
|
|
641
|
+
scopeId: selectionArg.head.storageKey,
|
|
642
|
+
'session.sessionId': sessionIdArg,
|
|
643
|
+
id: { $in: sessionRecords.map((record) => record.id) },
|
|
644
|
+
}, this.pageOptions(2, signalArg));
|
|
645
|
+
// A publication lists one record per session.
|
|
646
|
+
if (models.length > 1) throw new FlexProjectionFormatError();
|
|
647
|
+
const [model] = models;
|
|
648
|
+
if (!model) throw new FlexProjectionNotFoundError();
|
|
649
|
+
const document = projectSessionRecord(model);
|
|
650
|
+
assertFlexPublicSessionRecordDocument(document);
|
|
651
|
+
const manifest = sessionRecords.find((record) => record.id === document.id);
|
|
652
|
+
if (!manifest || document.session.sessionId !== sessionIdArg) throw new FlexProjectionFormatError();
|
|
653
|
+
verifyRecordManifest(document, manifest);
|
|
654
|
+
return document;
|
|
655
|
+
}
|
|
656
|
+
|
|
630
657
|
/**
|
|
631
658
|
* The transcript the selected publication names for a session, proven by its id; a session
|
|
632
659
|
* without messages has none.
|
|
@@ -73,7 +73,7 @@ import {
|
|
|
73
73
|
readOnlyFlexRequestMethods,
|
|
74
74
|
type IFlexDriverResolution,
|
|
75
75
|
} from './interfaces.flexipc.js';
|
|
76
|
-
import {
|
|
76
|
+
import { driverNotesContext, driverToolDefinitions } from './driver/functions.drivertools.js';
|
|
77
77
|
import { flexClaudePoolConnectionId, parseFlexAuthorityConnectionId } from '../ts_interfaces/functions.authorityscope.js';
|
|
78
78
|
import { createFailureReference, describeFailure } from './functions.failuredescription.js';
|
|
79
79
|
import { resolveConfiguredFlexMcpServers } from './configuration/functions.flexmcp.js';
|
|
@@ -92,6 +92,13 @@ type TFlexAuthorityAccessTarget = { scopeId: string } | { purpose: TFlexInstalla
|
|
|
92
92
|
type TFlexDelegatedRunAdmissionProviderContext =
|
|
93
93
|
plugins.flexharness.IFlexDelegatedRunAdmissionContext<IFlexProjectScope>;
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* The driver's cache setting on Anthropic: one-hour breakpoints. The controller keeps the driver's cache warm for an
|
|
97
|
+
* hour after its board last changed (`fleet.driver.keepCacheWarm`), and FlexHarness times its warm turns by the TTL a
|
|
98
|
+
* call wrote, so an idle hour costs one warm turn instead of one every four minutes. Other providers keep their
|
|
99
|
+
* default: the ChatGPT backend caches in memory and takes no retention.
|
|
100
|
+
*/
|
|
101
|
+
const flexDriverAnthropicCache: plugins.flexharness.TFlexAgentCacheSetting = { retention: '1h' };
|
|
95
102
|
const flexFailureStatusTimeoutMs = 1_000;
|
|
96
103
|
|
|
97
104
|
export const flexHarnessTimeoutPolicy = Object.freeze({
|
|
@@ -359,6 +366,7 @@ const projectSerializedMethods: ReadonlySet<TFlexRequestMethod> = new Set([
|
|
|
359
366
|
'session.reversion.info',
|
|
360
367
|
'slash.list',
|
|
361
368
|
'message.page',
|
|
369
|
+
'message.metadata.page',
|
|
362
370
|
'message.get',
|
|
363
371
|
'prompt.start',
|
|
364
372
|
'prompt.continue',
|
|
@@ -403,6 +411,7 @@ const projectScopedMethods: ReadonlySet<TFlexRequestMethod> = new Set([
|
|
|
403
411
|
'slash.list',
|
|
404
412
|
'slash.execute',
|
|
405
413
|
'message.page',
|
|
414
|
+
'message.metadata.page',
|
|
406
415
|
'message.get',
|
|
407
416
|
'prompt.start',
|
|
408
417
|
'prompt.continue',
|
|
@@ -1403,6 +1412,16 @@ export class FlexService {
|
|
|
1403
1412
|
}),
|
|
1404
1413
|
};
|
|
1405
1414
|
}
|
|
1415
|
+
case 'message.metadata.page': {
|
|
1416
|
+
const payload = payloadArg as IFlexRequestMap['message.metadata.page']['request'];
|
|
1417
|
+
return {
|
|
1418
|
+
result: await harness.listMessageMetadataPage(payload.scopeId, payload.sessionId, {
|
|
1419
|
+
...(payload.limit !== undefined ? { limit: payload.limit } : {}),
|
|
1420
|
+
...(payload.before !== undefined ? { before: payload.before } : {}),
|
|
1421
|
+
...(payload.includeUndone === undefined ? {} : { includeUndone: payload.includeUndone }),
|
|
1422
|
+
}),
|
|
1423
|
+
};
|
|
1424
|
+
}
|
|
1406
1425
|
case 'message.get': {
|
|
1407
1426
|
const payload = payloadArg as IFlexRequestMap['message.get']['request'];
|
|
1408
1427
|
return {
|
|
@@ -2340,11 +2359,18 @@ export class FlexService {
|
|
|
2340
2359
|
private async resolveConfiguredRunModel(
|
|
2341
2360
|
contextArg: Readonly<plugins.flexharness.IFlexModelResolverContext<IFlexProjectScope>>,
|
|
2342
2361
|
): Promise<TFlexResolvedModel> {
|
|
2343
|
-
const
|
|
2362
|
+
const model = await this.resolveModel(
|
|
2344
2363
|
contextArg.scopeId, contextArg.signal, contextArg.modelHint, contextArg, contextArg.sessionId,
|
|
2345
2364
|
);
|
|
2346
2365
|
const driver = await this.resolveDriver(contextArg);
|
|
2347
|
-
|
|
2366
|
+
// The driver's system prompt holds its stable instructions only; its notes are a pinned
|
|
2367
|
+
// context, so a notes update never rewrites the cached prefix.
|
|
2368
|
+
const resolved: TFlexResolvedModel = driver ? {
|
|
2369
|
+
...model,
|
|
2370
|
+
...(model.identity.provider === 'anthropic' ? { cache: flexDriverAnthropicCache } : {}),
|
|
2371
|
+
pinnedContext: driverNotesContext(driver.notes, driver.notesRevision),
|
|
2372
|
+
} : model;
|
|
2373
|
+
const driverSystem = driver?.instructions.trim();
|
|
2348
2374
|
const key = this.configurationSessionKey(contextArg);
|
|
2349
2375
|
const projection = this.configuredSessions.get(key);
|
|
2350
2376
|
if (!projection) {
|
|
@@ -2363,7 +2389,8 @@ export class FlexService {
|
|
|
2363
2389
|
|
|
2364
2390
|
/**
|
|
2365
2391
|
* The controller says whether a root run belongs to the host's driver session, with the driver's
|
|
2366
|
-
* instructions and notes. A run reads them once, so its system
|
|
2392
|
+
* instructions and its notes with their scratchpad revision. A run reads them once, so its system
|
|
2393
|
+
* prompt and pinned notes stay stable while it runs.
|
|
2367
2394
|
*/
|
|
2368
2395
|
private resolveDriver(contextArg: Readonly<{
|
|
2369
2396
|
scopeId: string;
|
|
@@ -4114,7 +4141,8 @@ export class FlexService {
|
|
|
4114
4141
|
baseTextUtf8Bytes: eventArg.baseTextUtf8Bytes,
|
|
4115
4142
|
textUtf8Bytes: eventArg.textUtf8Bytes,
|
|
4116
4143
|
});
|
|
4117
|
-
|
|
4144
|
+
// Content progress travels only as the delta: the part's start and completion, and the
|
|
4145
|
+
// run's lifecycle, carry the session change (updated time, preview, status).
|
|
4118
4146
|
return;
|
|
4119
4147
|
}
|
|
4120
4148
|
if (
|