@ours.network/fleet 1.2.0-nightly.82 → 1.2.0-nightly.83

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.
@@ -25,9 +25,6 @@ export declare function resumedConversationPage(stateDir: string, request: {
25
25
  }, live: ConversationPageView, expectedSessionId: string): Promise<ConversationPageView>;
26
26
  /** Download authorization requires an actual published attachment, never an orphan copy. */
27
27
  export declare function conversationHasAttachment(stateDir: string, attachment: import('../file-delivery/types.js').DeliveredFile): Promise<boolean>;
28
- /** Passive, bounded last assistant text for directory previews. Never opens the
29
- * ledger writer/recovery path or acquires a session/controller. An incomplete
30
- * newest append is not committed yet; completed malformed records still fail. */
31
28
  export declare function conversationPreview(stateDir: string, signal: AbortSignal): Promise<{
32
29
  text: string;
33
30
  at: string;
@@ -230,6 +230,60 @@ export async function conversationHasAttachment(stateDir, attachment) {
230
230
  /** Passive, bounded last assistant text for directory previews. Never opens the
231
231
  * ledger writer/recovery path or acquires a session/controller. An incomplete
232
232
  * newest append is not committed yet; completed malformed records still fail. */
233
+ /** Bytes of a segment's tail a preview may consider, and the newest events it reads. */
234
+ const PREVIEW_WINDOW_BYTES = 2 * 1024 * 1024;
235
+ const PREVIEW_EVENTS = 200;
236
+ /**
237
+ * Newest-first completed events from the last PREVIEW_WINDOW_BYTES of one
238
+ * segment, read backwards in chunks until `need` events are found. A record
239
+ * cut by the window start is not complete and is skipped. Only the newest
240
+ * segment can have an in-flight append; elsewhere a partial record fails.
241
+ */
242
+ async function previewSegmentEvents(path, newest, need, signal) {
243
+ const events = [];
244
+ const file = await open(path, 'r');
245
+ try {
246
+ const size = (await file.stat()).size, floor = Math.max(0, size - PREVIEW_WINDOW_BYTES);
247
+ let position = size, carry = Buffer.alloc(0), trailing = true;
248
+ const take = (line) => {
249
+ const event = JSON.parse(line);
250
+ if (event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq))
251
+ throw Error('preview ledger invalid');
252
+ events.push(event);
253
+ };
254
+ while (position > floor && events.length < need) {
255
+ signal.throwIfAborted();
256
+ const length = Math.min(64 * 1024, position - floor);
257
+ position -= length;
258
+ const chunk = Buffer.alloc(length);
259
+ await file.read(chunk, 0, length, position);
260
+ const buffer = Buffer.concat([chunk, carry]);
261
+ let end = buffer.length;
262
+ for (let i = buffer.length - 1; i >= 0 && events.length < need; i--) {
263
+ if (buffer[i] !== 10)
264
+ continue;
265
+ if (trailing) {
266
+ if (end > i + 1 && !newest)
267
+ throw Error('preview ledger has partial record');
268
+ trailing = false;
269
+ }
270
+ else
271
+ take(buffer.toString('utf8', i + 1, end));
272
+ end = i;
273
+ }
274
+ carry = buffer.subarray(0, end);
275
+ }
276
+ if (trailing && position === floor && (!newest || floor > 0) && size > 0)
277
+ throw Error('preview ledger has partial record');
278
+ // At the file start the remaining bytes are the first complete record.
279
+ if (!trailing && floor === 0 && position === 0 && events.length < need)
280
+ take(carry.toString('utf8'));
281
+ }
282
+ finally {
283
+ await file.close();
284
+ }
285
+ return events;
286
+ }
233
287
  export async function conversationPreview(stateDir, signal) {
234
288
  signal.throwIfAborted();
235
289
  const dir = join(stateDir, '.conversation');
@@ -245,30 +299,8 @@ export async function conversationPreview(stateDir, signal) {
245
299
  const events = [];
246
300
  for (const name of files) {
247
301
  signal.throwIfAborted();
248
- const file = await open(join(dir, name), 'r');
249
- try {
250
- const size = (await file.stat()).size, length = Math.min(size, 2 * 1024 * 1024), buffer = Buffer.alloc(length);
251
- await file.read(buffer, 0, length, size - length);
252
- const completeEnd = buffer.lastIndexOf(10) + 1;
253
- // Only the newest segment can have an in-flight append. If its tail
254
- // fills the bounded window, we cannot establish the previous preview.
255
- if (completeEnd < length && (name !== files[0] || (size > length && completeEnd === 0)))
256
- throw Error('preview ledger has partial record');
257
- const lines = buffer.toString('utf8', 0, completeEnd).split('\n');
258
- lines.pop();
259
- if (size > length)
260
- lines.shift();
261
- for (let i = lines.length - 1; i >= 0 && events.length < 200; i--) {
262
- const event = JSON.parse(lines[i]);
263
- if (event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq))
264
- throw Error('preview ledger invalid');
265
- events.push(event);
266
- }
267
- }
268
- finally {
269
- await file.close();
270
- }
271
- if (events.length >= 200)
302
+ events.push(...await previewSegmentEvents(join(dir, name), name === files[0], PREVIEW_EVENTS - events.length, signal));
303
+ if (events.length >= PREVIEW_EVENTS)
272
304
  break;
273
305
  }
274
306
  signal.throwIfAborted();
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.82",
3
- "buildId": "79b7bd25e769",
4
- "commit": "0836a5d1d38495f31697ef62f286b9aa87442fc0",
2
+ "version": "1.2.0-nightly.83",
3
+ "buildId": "4171a7e61e16",
4
+ "commit": "dcb322603476266ea118965f0f8b5ba4e2017733",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-20T14:46:35.360Z",
6
+ "builtAt": "2026-10-20T18:25:32.708Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -62,6 +62,8 @@ export interface ResourceWireFrame {
62
62
  export declare const RESOURCE_FRAME_BYTES: number;
63
63
  export declare const RESOURCE_PENDING_BYTES: number;
64
64
  export declare const RESOURCE_ASSEMBLY_BYTES: number;
65
+ /** Minimum spacing of activity-only follow-up captures of one view. */
66
+ export declare const ACTIVITY_CAPTURE_INTERVAL_MS = 1000;
65
67
  /** Lazily yield byte chunks so backpressure never queues an entire large record.
66
68
  * Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
67
69
  export declare function resourceFrames(envelope: ResourceEnvelope): Generator<ResourceWireFrame>;
@@ -109,9 +111,10 @@ export declare class FleetResourceViews {
109
111
  private scope;
110
112
  private project;
111
113
  private configurationInputs;
114
+ private activityCaptureIntervalMs;
112
115
  readonly producerGeneration: `${string}-${string}-${string}-${string}-${string}`;
113
116
  private connections;
114
- constructor(scope: FleetResourceScope, project: (descriptor: ResourceDescriptor, signal: AbortSignal, owner: string) => Promise<unknown>, configurationInputs?: () => readonly string[]);
117
+ constructor(scope: FleetResourceScope, project: (descriptor: ResourceDescriptor, signal: AbortSignal, owner: string) => Promise<unknown>, configurationInputs?: () => readonly string[], activityCaptureIntervalMs?: number);
115
118
  open(response: ServerResponse, owner: string, authorized: () => boolean): {
116
119
  connectionId: `${string}-${string}-${string}-${string}-${string}`;
117
120
  close: () => void;
@@ -23,6 +23,8 @@ export const RESOURCE_FRAME_BYTES = 512 * 1024;
23
23
  export const RESOURCE_PENDING_BYTES = 8 * 1024 * 1024;
24
24
  export const RESOURCE_ASSEMBLY_BYTES = 16 * 1024 * 1024;
25
25
  const CHUNK_BYTES = 256 * 1024;
26
+ /** Minimum spacing of activity-only follow-up captures of one view. */
27
+ export const ACTIVITY_CAPTURE_INTERVAL_MS = 1000;
26
28
  /** Lazily yield byte chunks so backpressure never queues an entire large record.
27
29
  * Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
28
30
  export function* resourceFrames(envelope) {
@@ -187,12 +189,14 @@ export class FleetResourceViews {
187
189
  scope;
188
190
  project;
189
191
  configurationInputs;
192
+ activityCaptureIntervalMs;
190
193
  producerGeneration = randomUUID();
191
194
  connections = new Map();
192
- constructor(scope, project, configurationInputs = () => []) {
195
+ constructor(scope, project, configurationInputs = () => [], activityCaptureIntervalMs = ACTIVITY_CAPTURE_INTERVAL_MS) {
193
196
  this.scope = scope;
194
197
  this.project = project;
195
198
  this.configurationInputs = configurationInputs;
199
+ this.activityCaptureIntervalMs = activityCaptureIntervalMs;
196
200
  }
197
201
  open(response, owner, authorized) {
198
202
  const id = randomUUID(), controller = new AbortController();
@@ -334,7 +338,7 @@ export class FleetResourceViews {
334
338
  return connection;
335
339
  }
336
340
  current(connection, view) { return !connection.controller.signal.aborted && !view.controller.signal.aborted && connection.views.get(view.descriptor.viewId) === view; }
337
- retire(view) { view.controller.abort(); view.rejectInitial(new ResourceBindingError(409, 'resource binding retired')); }
341
+ retire(view) { clearTimeout(view.activityTimer); view.activityTimer = undefined; view.controller.abort(); view.rejectInitial(new ResourceBindingError(409, 'resource binding retired')); }
338
342
  emit(connection, view, kind, payload, source) {
339
343
  return connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, viewId: view.descriptor.viewId, subscriptionEpoch: view.epoch, baselineId: view.baselineId, sequence: view.sequence + 1, revision: view.revision, kind, payload, ...(source ? { source } : {}) }, () => { view.sequence++; if (source || kind === 'ready')
340
344
  view.qualifiedAt = Date.now(); });
@@ -388,10 +392,10 @@ export class FleetResourceViews {
388
392
  return false;
389
393
  // Only classify known activity writes under the configured role roots.
390
394
  // Parent/unknown events and every other file retain full invalidation.
391
- // The canonical web producer atomically replaces hashed checkpoint files.
392
- // These delivery acknowledgements are not role/task/config membership;
393
- // retain full wakes without requiring an outbox-quiet capture window.
394
- if (structural && /^[a-f0-9]{64}\.json$/.test(relative(resolve(stateRoot(), 'web', 'notification-outboxes'), path)))
395
+ // The canonical web producer atomically replaces hashed checkpoint files,
396
+ // once per ledger advance of any role. No projection reads these delivery
397
+ // acknowledgements, so they neither fence a capture nor wake a follow-up.
398
+ if (/^[a-f0-9]{64}\.json$/.test(relative(resolve(stateRoot(), 'web', 'notification-outboxes'), path)))
395
399
  return false;
396
400
  for (const root of roots) {
397
401
  const parts = relative(resolve(root), path).split(sep);
@@ -421,9 +425,28 @@ export class FleetResourceViews {
421
425
  }
422
426
  return true;
423
427
  }
424
- schedule(connection, view) {
428
+ schedule(connection, view, due = false) {
425
429
  if (!this.current(connection, view) || view.failed)
426
430
  return;
431
+ // Agent activity (ledger appends, status files) changes previews and status
432
+ // continuously while an agent works. Without a structural change since the
433
+ // latest capture began, coalesce those wakes into one trailing capture per
434
+ // interval instead of re-projecting every role after every append.
435
+ const wait = !due && view.captureSource !== view.source && view.capturedAt !== undefined
436
+ && view.captureSource.snapshot().position === view.capturedStructure
437
+ ? this.activityCaptureIntervalMs - (Date.now() - view.capturedAt) : 0;
438
+ if (wait > 0) {
439
+ view.scheduledPosition = view.source.snapshot().position;
440
+ view.dirty = true;
441
+ if (!view.running && !view.activityTimer) {
442
+ // The trailing capture runs when its timer fires, whatever the clock reads.
443
+ view.activityTimer = setTimeout(() => { view.activityTimer = undefined; this.schedule(connection, view, true); }, Math.min(wait, this.activityCaptureIntervalMs));
444
+ view.activityTimer.unref?.();
445
+ }
446
+ return;
447
+ }
448
+ clearTimeout(view.activityTimer);
449
+ view.activityTimer = undefined;
427
450
  view.scheduledPosition = view.source.snapshot().position;
428
451
  view.dirty = true;
429
452
  if (view.running)
@@ -446,6 +469,8 @@ export class FleetResourceViews {
446
469
  if (!view.dirty || !this.current(connection, view))
447
470
  return;
448
471
  view.dirty = false;
472
+ view.capturedAt = Date.now();
473
+ view.capturedStructure = view.captureSource.snapshot().position;
449
474
  let payload, source;
450
475
  for (let attempt = 0; attempt < 3; attempt++) {
451
476
  view.controller.signal.throwIfAborted();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.82",
3
+ "version": "1.2.0-nightly.83",
4
4
  "description": "Harness-agnostic fleet of persistent, identity-bound AI agents. Declarative fleet.yaml, managed native/ACP sessions, supervision, and ours.network messaging.",
5
5
  "type": "module",
6
6
  "license": "FSL-1.1-Apache-2.0",