@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.
- package/dist/application/session-control.js +16 -7
- package/dist/build-info.json +4 -4
- package/dist/session/control.js +8 -2
- package/dist/web/conversation-stream.d.ts +1 -0
- package/dist/web/conversation-stream.js +10 -2
- package/dist/web/resource-views.d.ts +12 -2
- package/dist/web/resource-views.js +52 -14
- package/package.json +5 -5
|
@@ -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 >=
|
|
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,
|
|
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
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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'));
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "1.2.0-nightly.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "1.2.0-nightly.78",
|
|
3
|
+
"buildId": "b2ec2cc5b628",
|
|
4
|
+
"commit": "42ac52e4fadbfe48faac20ac2518d7280947c981",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-19T21:24:11.994Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
package/dist/session/control.js
CHANGED
|
@@ -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(() =>
|
|
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
|
-
},
|
|
526
|
+
}, 15_000);
|
|
521
527
|
heartbeat.unref();
|
|
522
528
|
}
|
|
523
529
|
socket.once('close', stop);
|
|
@@ -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:
|
|
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:
|
|
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.
|
|
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.
|
|
52
|
-
"@ours.network/mcp": "1.2.0-nightly.
|
|
53
|
-
"@ours.network/sdk": "4.1.1-nightly.
|
|
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.
|
|
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",
|