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.
Files changed (137) hide show
  1. package/changelog.md +43 -0
  2. package/dist_serve/bundle.js +3514 -3357
  3. package/dist_serve/bundle.js.map +2 -2
  4. package/dist_serve/bundle.js.third-party-notices.json +58 -32
  5. package/dist_serve/bundle.js.third-party-notices.txt +108 -33
  6. package/dist_ts/00_commitinfo_data.js +1 -1
  7. package/dist_ts/aglapi/aglapisurface.document.js +64 -37
  8. package/dist_ts/classes.aglhome.d.ts +5 -0
  9. package/dist_ts/classes.aglhome.js +2 -1
  10. package/dist_ts/classes.authstore.js +5 -1
  11. package/dist_ts/classes.controller.d.ts +8 -0
  12. package/dist_ts/classes.controller.js +119 -22
  13. package/dist_ts/classes.controllereventdelivery.d.ts +72 -0
  14. package/dist_ts/classes.controllereventdelivery.js +233 -0
  15. package/dist_ts/classes.flexmodels.public.js +4 -1
  16. package/dist_ts/classes.flexprojectionreader.d.ts +6 -0
  17. package/dist_ts/classes.flexprojectionreader.js +29 -5
  18. package/dist_ts/classes.flexservice.d.ts +2 -1
  19. package/dist_ts/classes.flexservice.js +34 -6
  20. package/dist_ts/classes.flexstore.d.ts +41 -4
  21. package/dist_ts/classes.flexstore.js +266 -203
  22. package/dist_ts/classes.inferencegateway.d.ts +35 -1
  23. package/dist_ts/classes.inferencegateway.js +86 -26
  24. package/dist_ts/classes.inferenceservice.d.ts +4 -2
  25. package/dist_ts/classes.inferenceservice.js +5 -4
  26. package/dist_ts/classes.proxycontrol.d.ts +58 -4
  27. package/dist_ts/classes.proxycontrol.js +23 -44
  28. package/dist_ts/classes.proxycontrolclient.d.ts +15 -5
  29. package/dist_ts/classes.proxycontrolclient.js +67 -17
  30. package/dist_ts/classes.proxydaemon.d.ts +22 -3
  31. package/dist_ts/classes.proxydaemon.js +73 -12
  32. package/dist_ts/classes.proxyleaseholder.d.ts +43 -14
  33. package/dist_ts/classes.proxyleaseholder.js +66 -22
  34. package/dist_ts/classes.proxystore.d.ts +9 -0
  35. package/dist_ts/classes.proxystore.js +15 -7
  36. package/dist_ts/classes.ticketnotifier.js +4 -2
  37. package/dist_ts/classes.ticketstore.d.ts +50 -2
  38. package/dist_ts/classes.ticketstore.js +195 -78
  39. package/dist_ts/classes.workercallerbindings.d.ts +66 -0
  40. package/dist_ts/classes.workercallerbindings.js +211 -0
  41. package/dist_ts/classes.workercallers.d.ts +63 -2
  42. package/dist_ts/classes.workercallers.js +430 -65
  43. package/dist_ts/driver/classes.drivereventrelay.d.ts +13 -1
  44. package/dist_ts/driver/classes.drivereventrelay.js +99 -6
  45. package/dist_ts/driver/functions.driverinstructions.d.ts +1 -1
  46. package/dist_ts/driver/functions.driverinstructions.js +1 -1
  47. package/dist_ts/driver/functions.drivertools.d.ts +11 -2
  48. package/dist_ts/driver/functions.drivertools.js +19 -9
  49. package/dist_ts/fleet/classes.boardpump.d.ts +8 -2
  50. package/dist_ts/fleet/classes.boardpump.js +38 -20
  51. package/dist_ts/fleet/classes.ticketeffects.d.ts +17 -2
  52. package/dist_ts/fleet/classes.ticketeffects.js +16 -10
  53. package/dist_ts/fleet/classes.workerlaunches.d.ts +12 -5
  54. package/dist_ts/fleet/classes.workerlaunches.js +25 -19
  55. package/dist_ts/fleet/classes.workers.d.ts +21 -0
  56. package/dist_ts/fleet/classes.workers.js +110 -17
  57. package/dist_ts/fleet/functions.workervalidation.js +2 -2
  58. package/dist_ts/functions.flexprivatesnapshot.d.ts +102 -0
  59. package/dist_ts/functions.flexprivatesnapshot.js +461 -0
  60. package/dist_ts/functions.inferencecli.d.ts +3 -2
  61. package/dist_ts/functions.inferencecli.js +67 -5
  62. package/dist_ts/functions.inferencecliarguments.d.ts +6 -2
  63. package/dist_ts/functions.inferencecliarguments.js +29 -2
  64. package/dist_ts/functions.inferencelaunch.d.ts +24 -0
  65. package/dist_ts/functions.inferencelaunch.js +42 -2
  66. package/dist_ts/functions.mcptickettools.d.ts +3 -0
  67. package/dist_ts/functions.mcptickettools.js +2 -2
  68. package/dist_ts/functions.proxycontract.d.ts +11 -0
  69. package/dist_ts/functions.proxycontract.js +12 -1
  70. package/dist_ts/functions.proxysocket.d.ts +6 -0
  71. package/dist_ts/functions.proxysocket.js +46 -0
  72. package/dist_ts/functions.ticketcli.d.ts +1 -1
  73. package/dist_ts/functions.ticketcli.js +8 -4
  74. package/dist_ts/functions.ticketvalidation.d.ts +3 -0
  75. package/dist_ts/functions.ticketvalidation.js +7 -4
  76. package/dist_ts/functions.workerprocessproof.d.ts +6 -0
  77. package/dist_ts/functions.workerprocessproof.js +28 -4
  78. package/dist_ts/interfaces.flexipc.d.ts +21 -2
  79. package/dist_ts/interfaces.flexipc.js +33 -16
  80. package/dist_ts_interfaces/fleetworkers.d.ts +6 -0
  81. package/dist_ts_interfaces/interfaces.d.ts +1 -1
  82. package/dist_ts_interfaces/interfaces.js +1 -1
  83. package/dist_ts_interfaces/requests.tickets.d.ts +8 -12
  84. package/dist_ts_interfaces/tickets.d.ts +37 -9
  85. package/dist_ts_interfaces/tickets.js +7 -1
  86. package/dist_ts_interfaces/workercallers.d.ts +43 -4
  87. package/package.json +7 -7
  88. package/readme.md +156 -23
  89. package/ts/00_commitinfo_data.ts +1 -1
  90. package/ts/aglapi/aglapisurface.document.ts +63 -36
  91. package/ts/classes.aglhome.ts +6 -0
  92. package/ts/classes.authstore.ts +4 -0
  93. package/ts/classes.controller.ts +141 -32
  94. package/ts/classes.controllereventdelivery.ts +289 -0
  95. package/ts/classes.flexmodels.public.ts +10 -0
  96. package/ts/classes.flexprojectionreader.ts +30 -3
  97. package/ts/classes.flexservice.ts +33 -5
  98. package/ts/classes.flexstore.ts +381 -246
  99. package/ts/classes.inferencegateway.ts +104 -23
  100. package/ts/classes.inferenceservice.ts +6 -3
  101. package/ts/classes.proxycontrol.ts +76 -39
  102. package/ts/classes.proxycontrolclient.ts +72 -20
  103. package/ts/classes.proxydaemon.ts +85 -15
  104. package/ts/classes.proxyleaseholder.ts +84 -28
  105. package/ts/classes.proxystore.ts +16 -5
  106. package/ts/classes.ticketnotifier.ts +3 -1
  107. package/ts/classes.ticketstore.ts +201 -75
  108. package/ts/classes.workercallerbindings.ts +147 -0
  109. package/ts/classes.workercallers.ts +410 -59
  110. package/ts/driver/classes.drivereventrelay.ts +89 -4
  111. package/ts/driver/functions.driverinstructions.ts +1 -1
  112. package/ts/driver/functions.drivertools.ts +22 -8
  113. package/ts/fleet/classes.boardpump.ts +51 -19
  114. package/ts/fleet/classes.ticketeffects.ts +25 -11
  115. package/ts/fleet/classes.workerlaunches.ts +23 -15
  116. package/ts/fleet/classes.workers.ts +79 -17
  117. package/ts/fleet/functions.workervalidation.ts +1 -1
  118. package/ts/functions.flexprivatesnapshot.ts +601 -0
  119. package/ts/functions.inferencecli.ts +61 -7
  120. package/ts/functions.inferencecliarguments.ts +32 -1
  121. package/ts/functions.inferencelaunch.ts +49 -2
  122. package/ts/functions.mcptickettools.ts +1 -1
  123. package/ts/functions.proxycontract.ts +12 -0
  124. package/ts/functions.proxysocket.ts +39 -0
  125. package/ts/functions.ticketcli.ts +7 -3
  126. package/ts/functions.ticketvalidation.ts +7 -3
  127. package/ts/functions.workerprocessproof.ts +27 -4
  128. package/ts/interfaces.flexipc.ts +50 -21
  129. package/ts_interfaces/fleetworkers.ts +6 -0
  130. package/ts_interfaces/interfaces.ts +1 -1
  131. package/ts_interfaces/requests.tickets.ts +9 -6
  132. package/ts_interfaces/tickets.ts +32 -1
  133. package/ts_interfaces/workercallers.ts +32 -4
  134. package/ts_web/00_commitinfo_data.ts +1 -1
  135. package/ts_web/elements.harnesscontrollerapp.ts +19 -8
  136. package/ts_web/elements.ticketboard.ts +16 -4
  137. 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 records = await this.loadSessionRecords(selection, signalArg);
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 { composeDriverSystem, driverToolDefinitions } from './driver/functions.drivertools.js';
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 resolved = await this.resolveModel(
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
- const driverSystem = driver ? composeDriverSystem(driver.instructions, driver.notes) : undefined;
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 prompt stays stable while it runs.
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
- this.queueSessionChange(eventArg.scopeId, eventArg.sessionId, eventArg);
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 (