@ours.network/fleet 2.1.0 → 2.1.1

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.1.0",
3
- "buildId": "697e8bf9e53c",
4
- "commit": "2805e0e84c2a3bad05efdbe895fd8fb6e9d2aff4",
2
+ "version": "2.1.1",
3
+ "buildId": "27ffd73f427b",
4
+ "commit": "7beeb5809663f045677d7ec92515f88ce0001145",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-27T12:22:22.859Z",
6
+ "builtAt": "2026-10-28T12:03:39.903Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -699,13 +699,27 @@ export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
699
699
  const socket = createConnection(controlSocketPath(stateDir));
700
700
  socket.setEncoding('utf8');
701
701
  return new Promise((resolve, reject) => {
702
+ let settled = false, expiry;
703
+ const settle = () => { settled = true; clearTimeout(timer); if (expiry)
704
+ clearImmediate(expiry); };
705
+ // Timers run before pending I/O. When this process was busy past the deadline, a reply
706
+ // already waiting on the socket is still unread: give it the next poll phase before
707
+ // declaring the supervisor silent. That is one event-loop turn, not added time; a reply
708
+ // arriving within that turn is accepted, and a silent supervisor fails in the same turn.
702
709
  const timer = setTimeout(() => {
703
- socket.destroy();
704
- reject(new SessionControlError('timeout', `the role control plane did not answer '${request.command}' within ${timeoutMs}ms`));
710
+ expiry = setImmediate(() => {
711
+ if (settled)
712
+ return;
713
+ settle();
714
+ socket.destroy();
715
+ reject(new SessionControlError('timeout', `the role control plane did not answer '${request.command}' within ${timeoutMs}ms`));
716
+ });
705
717
  }, timeoutMs);
706
718
  let buffer = '';
707
719
  socket.once('error', error => {
708
- clearTimeout(timer);
720
+ if (settled)
721
+ return;
722
+ settle();
709
723
  const code = error.code;
710
724
  const permissionHint = code === 'EPERM' || code === 'EACCES'
711
725
  ? `; the operating system denied access before '${request.command}' reached the supervisor. `
@@ -718,9 +732,9 @@ export async function controlRequest(stateDir, request, timeoutMs = 120_000) {
718
732
  socket.on('data', chunk => {
719
733
  buffer += chunk;
720
734
  const newline = buffer.indexOf('\n');
721
- if (newline < 0)
735
+ if (newline < 0 || settled)
722
736
  return;
723
- clearTimeout(timer);
737
+ settle();
724
738
  try {
725
739
  resolve(JSON.parse(buffer.slice(0, newline)));
726
740
  }
@@ -131,6 +131,8 @@ export declare class FleetResourceViews {
131
131
  private retire;
132
132
  private emit;
133
133
  private coverage;
134
+ /** Certify each view's last projected source; never a position its capture did not read. */
135
+ private cover;
134
136
  private affects;
135
137
  private schedule;
136
138
  private refresh;
@@ -25,6 +25,8 @@ export const RESOURCE_ASSEMBLY_BYTES = 16 * 1024 * 1024;
25
25
  const CHUNK_BYTES = 256 * 1024;
26
26
  /** Minimum spacing of activity-only follow-up captures of one view. */
27
27
  export const ACTIVITY_CAPTURE_INTERVAL_MS = 1000;
28
+ /** Sparse health: a current view is qualified at most this often without data. */
29
+ const COVERAGE_INTERVAL_MS = 15000;
28
30
  /** Lazily yield byte chunks so backpressure never queues an entire large record.
29
31
  * Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
30
32
  export function* resourceFrames(envelope) {
@@ -263,7 +265,7 @@ export class FleetResourceViews {
263
265
  this.coverage(connection).catch(close);
264
266
  if (!connection.views.size)
265
267
  response.write(': keepalive\n\n');
266
- }, 15000);
268
+ }, COVERAGE_INTERVAL_MS);
267
269
  healthTimer.unref();
268
270
  }
269
271
  return { connectionId: id, close };
@@ -347,7 +349,11 @@ export class FleetResourceViews {
347
349
  // complete batch behind any active chunk assembly and earlier data records.
348
350
  coverage(connection) {
349
351
  const now = Date.now();
350
- const eligible = [...connection.views.values()].filter(view => this.current(connection, view) && !view.failed && !view.running && !view.dirty && view.payload !== undefined && view.covered && view.covered.health === 'healthy' && view.covered.position === view.source.snapshot().position && now - (view.qualifiedAt ?? 0) >= 15000);
352
+ return this.cover(connection, [...connection.views.values()].filter(view => !view.running && !view.dirty && view.covered?.position === view.source.snapshot().position && now - (view.qualifiedAt ?? 0) >= COVERAGE_INTERVAL_MS), now);
353
+ }
354
+ /** Certify each view's last projected source; never a position its capture did not read. */
355
+ cover(connection, candidates, now) {
356
+ const eligible = candidates.filter(view => this.current(connection, view) && !view.failed && view.payload !== undefined && view.covered && view.covered.health === 'healthy');
351
357
  if (!eligible.length)
352
358
  return Promise.resolve();
353
359
  const views = eligible.map(view => ({ protocolVersion: 1, producerGeneration: this.producerGeneration, viewId: view.descriptor.viewId, subscriptionEpoch: view.epoch, baselineId: view.baselineId, sequence: view.sequence + 1, revision: view.revision, source: view.covered }));
@@ -502,6 +508,10 @@ export class FleetResourceViews {
502
508
  return;
503
509
  view.payload = payload;
504
510
  }
511
+ // Sustained activity can keep a view dirty at every health tick while its payload is
512
+ // unchanged; the completed capture still qualifies the source it read.
513
+ else if (Date.now() - (view.qualifiedAt ?? 0) >= COVERAGE_INTERVAL_MS)
514
+ await this.cover(connection, [view], Date.now());
505
515
  if (first) {
506
516
  await this.emit(connection, view, 'ready', { baselineId: view.baselineId, source });
507
517
  if (this.current(connection, view))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.1.0",
3
+ "version": "2.1.1",
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",