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

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.78",
3
+ "buildId": "b2ec2cc5b628",
4
+ "commit": "42ac52e4fadbfe48faac20ac2518d7280947c981",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-19T11:49:00.042Z",
6
+ "builtAt": "2026-10-19T21:24:11.994Z",
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;
@@ -1,6 +1,7 @@
1
1
  import { relative, resolve, sep } from 'node:path';
2
2
  import { agentsRoot, tmpRoot, stateRoot } from '../paths.js';
3
3
  import { randomUUID } from 'node:crypto';
4
+ import { homedir } from 'node:os';
4
5
  import { observeFleetResources } from '../application/resource-observer.js';
5
6
  export async function abortableProjection(read, signal) {
6
7
  signal.throwIfAborted();
@@ -64,7 +65,7 @@ export class ResourceOutput {
64
65
  response.once('close', this.#abort);
65
66
  }
66
67
  }
67
- send(envelope) {
68
+ send(envelope, admitted) {
68
69
  if (this.#closed)
69
70
  return Promise.reject(new Error('resource output closed'));
70
71
  const bytes = Buffer.byteLength(JSON.stringify(envelope));
@@ -73,6 +74,7 @@ export class ResourceOutput {
73
74
  if (this.#active && this.#pendingBytes + bytes > RESOURCE_PENDING_BYTES) {
74
75
  return this.#fail(new Error('resource pending limit exceeded'));
75
76
  }
77
+ admitted?.();
76
78
  return new Promise((resolve, reject) => {
77
79
  const item = { envelope, bytes, resolve, reject };
78
80
  if (!this.#active) {
@@ -108,7 +110,7 @@ export class ResourceOutput {
108
110
  try {
109
111
  while (this.#active && !this.#closed) {
110
112
  const item = this.#active;
111
- for (const frame of resourceFrames(item.envelope)) {
113
+ for (const frame of ('views' in item.envelope ? [{ event: 'resource-coverage', data: item.envelope }] : resourceFrames(item.envelope))) {
112
114
  if (this.#closed)
113
115
  break;
114
116
  const writable = this.response.write(`event: ${frame.event}\ndata: ${JSON.stringify(frame.data)}\n\n`);
@@ -192,11 +194,13 @@ export class FleetResourceViews {
192
194
  const id = randomUUID(), controller = new AbortController();
193
195
  let connection;
194
196
  let timer;
197
+ let healthTimer;
195
198
  const close = () => {
196
199
  if (controller.signal.aborted)
197
200
  return;
198
201
  controller.abort();
199
202
  clearInterval(timer);
203
+ clearInterval(healthTimer);
200
204
  this.connections.delete(id);
201
205
  if (connection) {
202
206
  for (const view of connection.views.values())
@@ -235,19 +239,24 @@ export class FleetResourceViews {
235
239
  close();
236
240
  }
237
241
  else {
242
+ // Preserve credential retirement timing independently of sparse wire health.
238
243
  timer = setInterval(() => {
239
244
  if (!authorized()) {
240
245
  close();
241
246
  return;
242
247
  }
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
248
  }, 2000);
250
249
  timer.unref();
250
+ healthTimer = setInterval(() => {
251
+ if (!authorized()) {
252
+ close();
253
+ return;
254
+ }
255
+ this.coverage(connection).catch(close);
256
+ if (!connection.views.size)
257
+ response.write(': keepalive\n\n');
258
+ }, 15000);
259
+ healthTimer.unref();
251
260
  }
252
261
  return { connectionId: id, close };
253
262
  }
@@ -321,10 +330,42 @@ export class FleetResourceViews {
321
330
  }
322
331
  current(connection, view) { return !connection.controller.signal.aborted && !view.controller.signal.aborted && connection.views.get(view.descriptor.viewId) === view; }
323
332
  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 });
333
+ emit(connection, view, kind, payload, source) {
334
+ 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')
335
+ view.qualifiedAt = Date.now(); });
336
+ }
337
+ // Admission allocates sequences atomically; the existing writer keeps this
338
+ // complete batch behind any active chunk assembly and earlier data records.
339
+ coverage(connection) {
340
+ const now = Date.now();
341
+ 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);
342
+ if (!eligible.length)
343
+ return Promise.resolve();
344
+ 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 }));
345
+ return connection.output.send({ protocolVersion: 1, producerGeneration: this.producerGeneration, views }, () => { for (const view of eligible) {
346
+ view.sequence++;
347
+ view.qualifiedAt = now;
348
+ } });
326
349
  }
327
350
  affects(descriptor, path, structural = false) {
351
+ // An explicitly selected manifest may use any pathname, including one
352
+ // otherwise reserved for an independent task receipt.
353
+ if (this.scope.configPath && resolve(path) === resolve(this.scope.configPath))
354
+ return true;
355
+ // Adopted Codex history, telemetry and cache commits are not canonical
356
+ // Fleet preview inputs. Keep credentials, unknown paths and directory-level
357
+ // events conservative, and never override explicit config or role roots.
358
+ const roots = this.scope.roots ?? [agentsRoot(), tmpRoot()];
359
+ if (descriptor.resource === 'previews'
360
+ && /^(?:history\.jsonl|models_cache\.json|(?:thread_history|state|logs|queue|goals)_\d+\.sqlite(?:-wal|-shm|-journal)?)$/.test(relative(resolve(homedir(), '.codex'), path))
361
+ && !roots.some(root => { const local = relative(resolve(root), path); return local === '' || (local !== '..' && !local.startsWith('..' + sep)); }))
362
+ return false;
363
+ // Preview selection reads role config/state and conversation ledgers, never
364
+ // task layout run/operation/brief receipts. A worker may commit several of
365
+ // these while that independent canonical preview read is in flight.
366
+ // Unknown paths and directory-level events retain full invalidation.
367
+ if (descriptor.resource === 'previews' && /^task-[A-Za-z0-9_-]+(?:\.op\.json|\.json|\.brief\.md)$/.test(relative(resolve(stateRoot(), 'layouts'), path)))
368
+ return false;
328
369
  // Only classify known activity writes under the configured role roots.
329
370
  // Parent/unknown events and every other file retain full invalidation.
330
371
  // The canonical web producer atomically replaces hashed checkpoint files.
@@ -332,7 +373,6 @@ export class FleetResourceViews {
332
373
  // retain full wakes without requiring an outbox-quiet capture window.
333
374
  if (structural && /^[a-f0-9]{64}\.json$/.test(relative(resolve(stateRoot(), 'web', 'notification-outboxes'), path)))
334
375
  return false;
335
- const roots = this.scope.roots ?? [agentsRoot(), tmpRoot()];
336
376
  for (const root of roots) {
337
377
  const parts = relative(resolve(root), path).split(sep);
338
378
  if (parts[0] === '..')
@@ -400,7 +440,7 @@ export class FleetResourceViews {
400
440
  view.covered = source;
401
441
  if (changed) {
402
442
  view.revision++;
403
- await this.emit(connection, view, 'snapshot', payload);
443
+ await this.emit(connection, view, 'snapshot', payload, source);
404
444
  if (!this.current(connection, view))
405
445
  return;
406
446
  view.payload = payload;
@@ -410,8 +450,6 @@ export class FleetResourceViews {
410
450
  if (this.current(connection, view))
411
451
  view.resolveInitial({ payload, source, producerGeneration: this.producerGeneration, revision: view.revision, baselineId: view.baselineId, descriptor: view.descriptor });
412
452
  }
413
- else
414
- await this.emit(connection, view, 'coverage', { source });
415
453
  }
416
454
  catch (error) {
417
455
  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.78",
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",