@ours.network/fleet 1.2.0-nightly.75 → 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.
- 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 +43 -13
- 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.77",
|
|
3
|
+
"buildId": "bba4bf03b747",
|
|
4
|
+
"commit": "9bdfe660ab6660b0aa75f2d2bc84254e2e811373",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-19T20:21:21.233Z",
|
|
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;
|
|
@@ -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:
|
|
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.
|
|
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.
|
|
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",
|