@ours.network/fleet 1.2.0-nightly.76 → 1.2.0-nightly.77

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.
@@ -207,13 +207,13 @@ export class RoleSessionControlAdapter {
207
207
  clearTimeout(silence);
208
208
  silence = setTimeout(() => {
209
209
  // Liveness only: no role/session snapshots, regardless of web traffic.
210
- if (Date.now() - lastControlHeartbeat >= 6_000) {
210
+ if (Date.now() - lastControlHeartbeat >= 45_000) {
211
211
  this.controlHealthy = false;
212
212
  finish('control-silent');
213
213
  }
214
214
  else
215
215
  armSilence();
216
- }, Math.max(1, 6_000 - (Date.now() - lastControlHeartbeat)));
216
+ }, Math.max(1, 45_000 - (Date.now() - lastControlHeartbeat)));
217
217
  silence.unref?.();
218
218
  };
219
219
  let follow;
@@ -267,11 +267,20 @@ export class RoleSessionControlAdapter {
267
267
  follow = await this.openFollow(this.stateDir, passive ? { controller: false, expectedGeneration: passive.expectedGeneration, after: passive.after, signal: passive.signal } : request.after, message => {
268
268
  if (closed)
269
269
  return;
270
- if (passive && message.controlHeartbeat === true) {
271
- lastControlHeartbeat = Date.now();
272
- this.controlHealthy = true;
273
- armSilence();
274
- return;
270
+ if (passive) {
271
+ // A delayed callback must not revive an already elapsed source deadline.
272
+ if (Date.now() - lastControlHeartbeat >= 45_000) {
273
+ this.controlHealthy = false;
274
+ finish('control-silent');
275
+ return;
276
+ }
277
+ if (message.controlHeartbeat === true || message.conversationReplay || message.conversationReady || message.conversationEvent || message.conversationState) {
278
+ lastControlHeartbeat = Date.now();
279
+ this.controlHealthy = true;
280
+ armSilence();
281
+ }
282
+ if (message.controlHeartbeat === true)
283
+ return;
275
284
  }
276
285
  if (message.ok === false) {
277
286
  finish(String(message.error ?? 'conversation follow refused'));
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.76",
3
- "buildId": "adbb39fb78cd",
4
- "commit": "54d1ea9c17b78705a6af6a9d83872078dbeaa41d",
2
+ "version": "1.2.0-nightly.77",
3
+ "buildId": "bba4bf03b747",
4
+ "commit": "9bdfe660ab6660b0aa75f2d2bc84254e2e811373",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-19T11:49:00.042Z",
6
+ "builtAt": "2026-10-19T20:21:21.233Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -467,8 +467,14 @@ export class RoleControlServer {
467
467
  let stopped = false;
468
468
  let heartbeat;
469
469
  let writing = Promise.resolve();
470
+ let lastQualifyingOutputAt = Date.now();
470
471
  const send = (message) => {
471
- const result = writing.then(() => writeMessage(message));
472
+ const result = writing.then(async () => {
473
+ if (message.controlHeartbeat === true && Date.now() - lastQualifyingOutputAt < 15_000)
474
+ return;
475
+ await writeMessage(message);
476
+ lastQualifyingOutputAt = Date.now();
477
+ });
472
478
  writing = result.catch(() => { });
473
479
  return result;
474
480
  };
@@ -517,7 +523,7 @@ export class RoleControlServer {
517
523
  heartbeatPending = true;
518
524
  void send({ controlHeartbeat: true }).catch(() => { stop(); socket.destroy(); })
519
525
  .finally(() => { heartbeatPending = false; });
520
- }, 2_000);
526
+ }, 15_000);
521
527
  heartbeat.unref();
522
528
  }
523
529
  socket.once('close', stop);
@@ -9,6 +9,7 @@ export declare class ConversationStreamWriter {
9
9
  private waiting;
10
10
  private ended;
11
11
  private sequence;
12
+ private lastQualifyingOutputAt;
12
13
  pendingBytes: number;
13
14
  get busy(): boolean;
14
15
  constructor(output: Writable, failed: (reason: string) => void);
@@ -8,6 +8,7 @@ export class ConversationStreamWriter {
8
8
  waiting = false;
9
9
  ended = false;
10
10
  sequence = 0;
11
+ lastQualifyingOutputAt = Date.now();
11
12
  pendingBytes = 0;
12
13
  get busy() { return Boolean(this.active); }
13
14
  constructor(output, failed) {
@@ -21,6 +22,8 @@ export class ConversationStreamWriter {
21
22
  send(type, payload) {
22
23
  if (this.ended)
23
24
  return false;
25
+ if (type === 'keepalive' && Date.now() - this.lastQualifyingOutputAt < 15_000)
26
+ return true;
24
27
  const id = String(++this.sequence);
25
28
  try {
26
29
  if (!this.active) {
@@ -38,6 +41,8 @@ export class ConversationStreamWriter {
38
41
  this.queue.push({ type, payload, id, bytes });
39
42
  this.pendingBytes += bytes;
40
43
  }
44
+ if (!this.ended)
45
+ this.lastQualifyingOutputAt = Date.now();
41
46
  return !this.ended;
42
47
  }
43
48
  catch {
@@ -96,6 +101,7 @@ export class ConversationStreamWriter {
96
101
  }
97
102
  }
98
103
  import { FleetError } from '../application/errors.js';
104
+ import { presentAttachmentEvent } from './agent-attachments.js';
99
105
  export function registerConversationStream(app, services, auth) {
100
106
  const connections = new Set();
101
107
  app.addHook('preClose', async () => { for (const close of connections)
@@ -146,10 +152,12 @@ export function registerConversationStream(app, services, auth) {
146
152
  resync('capability-unavailable', false);
147
153
  return;
148
154
  }
155
+ const stateDir = owner?.snapshot().stateDir;
156
+ const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
149
157
  const handle = await control.followConversation({ controller: false, expectedGeneration: generation, after, signal, roleObservation: owner,
150
158
  onReplay: page => { if (active() && valid()) {
151
159
  for (const event of page.events)
152
- writer.send('ledger', event);
160
+ writer.send('ledger', present(event));
153
161
  writer.send('ledger', { coveredThrough: page.coveredThrough });
154
162
  } },
155
163
  onReady: ready => {
@@ -160,7 +168,7 @@ export function registerConversationStream(app, services, auth) {
160
168
  }
161
169
  },
162
170
  onEvent: event => { if (active() && valid()) {
163
- writer.send('ledger', event);
171
+ writer.send('ledger', present(event));
164
172
  writer.send('ledger', { coveredThrough: event.seq });
165
173
  } },
166
174
  onState: state => { if (active() && valid())
@@ -9,6 +9,7 @@ export interface ResourceFrameBinding {
9
9
  readonly sequence: number;
10
10
  readonly revision: number;
11
11
  readonly baselineId?: string;
12
+ readonly source?: FleetResourceState;
12
13
  }
13
14
  export type ResourceHealthReason = 'auth_required' | 'unsupported' | 'upstream_unavailable';
14
15
  export type ResourceEnvelope = ResourceFrameBinding & ({
@@ -46,8 +47,16 @@ export type ResourceEnvelope = ResourceFrameBinding & ({
46
47
  readonly reason: ResourceHealthReason;
47
48
  };
48
49
  });
50
+ interface ResourceCoverage {
51
+ protocolVersion: 1;
52
+ producerGeneration: string;
53
+ views: Array<ResourceFrameBinding & {
54
+ source: FleetResourceState;
55
+ }>;
56
+ }
57
+ type ResourceOutputRecord = ResourceEnvelope | ResourceCoverage;
49
58
  export interface ResourceWireFrame {
50
- event: 'resource' | 'resource-chunk';
59
+ event: 'resource' | 'resource-chunk' | 'resource-coverage';
51
60
  data: Record<string, unknown>;
52
61
  }
53
62
  export declare const RESOURCE_FRAME_BYTES: number;
@@ -64,7 +73,7 @@ export declare class ResourceOutput {
64
73
  readonly signal: AbortSignal;
65
74
  readonly failed: (error: Error) => void;
66
75
  constructor(response: ServerResponse, signal: AbortSignal, failed: (error: Error) => void);
67
- send(envelope: ResourceEnvelope): Promise<void>;
76
+ send(envelope: ResourceOutputRecord, admitted?: () => void): Promise<void>;
68
77
  close(error?: Error): void;
69
78
  }
70
79
  export interface ResourceDescriptor {
@@ -117,6 +126,7 @@ export declare class FleetResourceViews {
117
126
  private current;
118
127
  private retire;
119
128
  private emit;
129
+ private coverage;
120
130
  private affects;
121
131
  private schedule;
122
132
  private refresh;
@@ -64,7 +64,7 @@ export class ResourceOutput {
64
64
  response.once('close', this.#abort);
65
65
  }
66
66
  }
67
- send(envelope) {
67
+ send(envelope, admitted) {
68
68
  if (this.#closed)
69
69
  return Promise.reject(new Error('resource output closed'));
70
70
  const bytes = Buffer.byteLength(JSON.stringify(envelope));
@@ -73,6 +73,7 @@ export class ResourceOutput {
73
73
  if (this.#active && this.#pendingBytes + bytes > RESOURCE_PENDING_BYTES) {
74
74
  return this.#fail(new Error('resource pending limit exceeded'));
75
75
  }
76
+ admitted?.();
76
77
  return new Promise((resolve, reject) => {
77
78
  const item = { envelope, bytes, resolve, reject };
78
79
  if (!this.#active) {
@@ -108,7 +109,7 @@ export class ResourceOutput {
108
109
  try {
109
110
  while (this.#active && !this.#closed) {
110
111
  const item = this.#active;
111
- for (const frame of resourceFrames(item.envelope)) {
112
+ for (const frame of ('views' in item.envelope ? [{ event: 'resource-coverage', data: item.envelope }] : resourceFrames(item.envelope))) {
112
113
  if (this.#closed)
113
114
  break;
114
115
  const writable = this.response.write(`event: ${frame.event}\ndata: ${JSON.stringify(frame.data)}\n\n`);
@@ -192,11 +193,13 @@ export class FleetResourceViews {
192
193
  const id = randomUUID(), controller = new AbortController();
193
194
  let connection;
194
195
  let timer;
196
+ let healthTimer;
195
197
  const close = () => {
196
198
  if (controller.signal.aborted)
197
199
  return;
198
200
  controller.abort();
199
201
  clearInterval(timer);
202
+ clearInterval(healthTimer);
200
203
  this.connections.delete(id);
201
204
  if (connection) {
202
205
  for (const view of connection.views.values())
@@ -235,19 +238,24 @@ export class FleetResourceViews {
235
238
  close();
236
239
  }
237
240
  else {
241
+ // Preserve credential retirement timing independently of sparse wire health.
238
242
  timer = setInterval(() => {
239
243
  if (!authorized()) {
240
244
  close();
241
245
  return;
242
246
  }
243
- for (const view of connection.views.values())
244
- if (!view.failed && view.covered && view.covered.position === view.source.snapshot().position)
245
- void this.emit(connection, view, 'coverage', { source: view.covered }).catch(close);
246
- // Keep the transport live with no subscribed views; never claim source coverage from bytes.
247
- if (!connection.views.size)
248
- response.write(': keepalive\n\n');
249
247
  }, 2000);
250
248
  timer.unref();
249
+ healthTimer = setInterval(() => {
250
+ if (!authorized()) {
251
+ close();
252
+ return;
253
+ }
254
+ this.coverage(connection).catch(close);
255
+ if (!connection.views.size)
256
+ response.write(': keepalive\n\n');
257
+ }, 15000);
258
+ healthTimer.unref();
251
259
  }
252
260
  return { connectionId: id, close };
253
261
  }
@@ -321,10 +329,34 @@ export class FleetResourceViews {
321
329
  }
322
330
  current(connection, view) { return !connection.controller.signal.aborted && !view.controller.signal.aborted && connection.views.get(view.descriptor.viewId) === view; }
323
331
  retire(view) { view.controller.abort(); view.rejectInitial(new ResourceBindingError(409, 'resource binding retired')); }
324
- emit(connection, view, kind, payload) {
325
- return connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, viewId: view.descriptor.viewId, subscriptionEpoch: view.epoch, baselineId: view.baselineId, sequence: ++view.sequence, revision: view.revision, kind, payload });
332
+ emit(connection, view, kind, payload, source) {
333
+ 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')
334
+ view.qualifiedAt = Date.now(); });
335
+ }
336
+ // Admission allocates sequences atomically; the existing writer keeps this
337
+ // complete batch behind any active chunk assembly and earlier data records.
338
+ coverage(connection) {
339
+ const now = Date.now();
340
+ 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);
341
+ if (!eligible.length)
342
+ return Promise.resolve();
343
+ 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 }));
344
+ return connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, views }, () => { for (const view of eligible) {
345
+ view.sequence++;
346
+ view.qualifiedAt = now;
347
+ } });
326
348
  }
327
349
  affects(descriptor, path, structural = false) {
350
+ // An explicitly selected manifest may use any pathname, including one
351
+ // otherwise reserved for an independent task receipt.
352
+ if (this.scope.configPath && resolve(path) === resolve(this.scope.configPath))
353
+ return true;
354
+ // Preview selection reads role config/state and conversation ledgers, never
355
+ // task layout run/operation/brief receipts. A worker may commit several of
356
+ // these while that independent canonical preview read is in flight.
357
+ // Unknown paths and directory-level events retain full invalidation.
358
+ if (descriptor.resource === 'previews' && /^task-[A-Za-z0-9_-]+(?:\.op\.json|\.json|\.brief\.md)$/.test(relative(resolve(stateRoot(), 'layouts'), path)))
359
+ return false;
328
360
  // Only classify known activity writes under the configured role roots.
329
361
  // Parent/unknown events and every other file retain full invalidation.
330
362
  // The canonical web producer atomically replaces hashed checkpoint files.
@@ -400,7 +432,7 @@ export class FleetResourceViews {
400
432
  view.covered = source;
401
433
  if (changed) {
402
434
  view.revision++;
403
- await this.emit(connection, view, 'snapshot', payload);
435
+ await this.emit(connection, view, 'snapshot', payload, source);
404
436
  if (!this.current(connection, view))
405
437
  return;
406
438
  view.payload = payload;
@@ -410,8 +442,6 @@ export class FleetResourceViews {
410
442
  if (this.current(connection, view))
411
443
  view.resolveInitial({ payload, source, producerGeneration: this.producerGeneration, revision: view.revision, baselineId: view.baselineId, descriptor: view.descriptor });
412
444
  }
413
- else
414
- await this.emit(connection, view, 'coverage', { source });
415
445
  }
416
446
  catch (error) {
417
447
  if (!this.current(connection, view))
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.76",
3
+ "version": "1.2.0-nightly.77",
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",
@@ -48,9 +48,9 @@
48
48
  "dependencies": {
49
49
  "@agentclientprotocol/sdk": "^1.3.0",
50
50
  "@fastify/websocket": "^11.2.0",
51
- "@ours.network/cli": "3.1.1-nightly.2",
52
- "@ours.network/mcp": "1.2.0-nightly.16",
53
- "@ours.network/sdk": "4.1.1-nightly.2",
51
+ "@ours.network/cli": "3.1.1-nightly.4",
52
+ "@ours.network/mcp": "1.2.0-nightly.18",
53
+ "@ours.network/sdk": "4.1.1-nightly.4",
54
54
  "commander": "^12.1.0",
55
55
  "fastify": "^5.4.0",
56
56
  "qrcode": "1.5.4",
@@ -64,7 +64,7 @@
64
64
  "@agentclientprotocol/codex-acp": "1.10.0"
65
65
  },
66
66
  "devDependencies": {
67
- "@ours.network/daemon": "4.1.1-nightly.2",
67
+ "@ours.network/daemon": "4.1.1-nightly.4",
68
68
  "@types/node": "^22.20.4",
69
69
  "@types/qrcode": "^1.5.6",
70
70
  "@types/ws": "^8.18.1",