@ours.network/fleet 1.2.0-nightly.82 → 1.2.0-nightly.84
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/conversation-history.d.ts +0 -3
- package/dist/application/conversation-history.js +70 -25
- package/dist/application/role-repository.d.ts +2 -0
- package/dist/application/role-repository.js +65 -53
- package/dist/build-info.json +4 -4
- package/dist/web/resource-views.d.ts +4 -1
- package/dist/web/resource-views.js +32 -7
- package/package.json +1 -1
|
@@ -25,9 +25,6 @@ export declare function resumedConversationPage(stateDir: string, request: {
|
|
|
25
25
|
}, live: ConversationPageView, expectedSessionId: string): Promise<ConversationPageView>;
|
|
26
26
|
/** Download authorization requires an actual published attachment, never an orphan copy. */
|
|
27
27
|
export declare function conversationHasAttachment(stateDir: string, attachment: import('../file-delivery/types.js').DeliveredFile): Promise<boolean>;
|
|
28
|
-
/** Passive, bounded last assistant text for directory previews. Never opens the
|
|
29
|
-
* ledger writer/recovery path or acquires a session/controller. An incomplete
|
|
30
|
-
* newest append is not committed yet; completed malformed records still fail. */
|
|
31
28
|
export declare function conversationPreview(stateDir: string, signal: AbortSignal): Promise<{
|
|
32
29
|
text: string;
|
|
33
30
|
at: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createReadStream } from 'node:fs';
|
|
2
|
-
import { open, readFile, readdir } from 'node:fs/promises';
|
|
2
|
+
import { open, readFile, readdir, stat } from 'node:fs/promises';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { createInterface } from 'node:readline';
|
|
5
5
|
const HISTORICAL_KINDS = ['file.attached', 'fleet.task_created', 'prompt.admitted', 'message.chunk', 'message.replace', 'tool.upsert', 'tool.content_chunk', 'turn.completed'];
|
|
@@ -40,8 +40,15 @@ async function readLinesBackward(path, visit, signal) {
|
|
|
40
40
|
await file.close();
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
|
+
// Rotated segments never change, so their last cursor is reused until the file itself changes.
|
|
44
|
+
const lastSeqs = new Map();
|
|
43
45
|
/** The last event cursor in a segment, read from its tail; undefined when none is readable. */
|
|
44
46
|
async function lastSeq(path) {
|
|
47
|
+
const info = await stat(path).catch(() => undefined);
|
|
48
|
+
const key = info && `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}`;
|
|
49
|
+
const known = key ? lastSeqs.get(path) : undefined;
|
|
50
|
+
if (key && known?.key === key)
|
|
51
|
+
return known.seq;
|
|
45
52
|
let seq;
|
|
46
53
|
await readLinesBackward(path, line => { try {
|
|
47
54
|
const value = Number(JSON.parse(line).seq);
|
|
@@ -51,6 +58,12 @@ async function lastSeq(path) {
|
|
|
51
58
|
}
|
|
52
59
|
}
|
|
53
60
|
catch { } return true; });
|
|
61
|
+
if (key) {
|
|
62
|
+
lastSeqs.delete(path);
|
|
63
|
+
lastSeqs.set(path, { key, seq });
|
|
64
|
+
if (lastSeqs.size > 4096)
|
|
65
|
+
lastSeqs.delete(lastSeqs.keys().next().value);
|
|
66
|
+
}
|
|
54
67
|
return seq;
|
|
55
68
|
}
|
|
56
69
|
/**
|
|
@@ -230,6 +243,60 @@ export async function conversationHasAttachment(stateDir, attachment) {
|
|
|
230
243
|
/** Passive, bounded last assistant text for directory previews. Never opens the
|
|
231
244
|
* ledger writer/recovery path or acquires a session/controller. An incomplete
|
|
232
245
|
* newest append is not committed yet; completed malformed records still fail. */
|
|
246
|
+
/** Bytes of a segment's tail a preview may consider, and the newest events it reads. */
|
|
247
|
+
const PREVIEW_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
248
|
+
const PREVIEW_EVENTS = 200;
|
|
249
|
+
/**
|
|
250
|
+
* Newest-first completed events from the last PREVIEW_WINDOW_BYTES of one
|
|
251
|
+
* segment, read backwards in chunks until `need` events are found. A record
|
|
252
|
+
* cut by the window start is not complete and is skipped. Only the newest
|
|
253
|
+
* segment can have an in-flight append; elsewhere a partial record fails.
|
|
254
|
+
*/
|
|
255
|
+
async function previewSegmentEvents(path, newest, need, signal) {
|
|
256
|
+
const events = [];
|
|
257
|
+
const file = await open(path, 'r');
|
|
258
|
+
try {
|
|
259
|
+
const size = (await file.stat()).size, floor = Math.max(0, size - PREVIEW_WINDOW_BYTES);
|
|
260
|
+
let position = size, carry = Buffer.alloc(0), trailing = true;
|
|
261
|
+
const take = (line) => {
|
|
262
|
+
const event = JSON.parse(line);
|
|
263
|
+
if (event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq))
|
|
264
|
+
throw Error('preview ledger invalid');
|
|
265
|
+
events.push(event);
|
|
266
|
+
};
|
|
267
|
+
while (position > floor && events.length < need) {
|
|
268
|
+
signal.throwIfAborted();
|
|
269
|
+
const length = Math.min(64 * 1024, position - floor);
|
|
270
|
+
position -= length;
|
|
271
|
+
const chunk = Buffer.alloc(length);
|
|
272
|
+
await file.read(chunk, 0, length, position);
|
|
273
|
+
const buffer = Buffer.concat([chunk, carry]);
|
|
274
|
+
let end = buffer.length;
|
|
275
|
+
for (let i = buffer.length - 1; i >= 0 && events.length < need; i--) {
|
|
276
|
+
if (buffer[i] !== 10)
|
|
277
|
+
continue;
|
|
278
|
+
if (trailing) {
|
|
279
|
+
if (end > i + 1 && !newest)
|
|
280
|
+
throw Error('preview ledger has partial record');
|
|
281
|
+
trailing = false;
|
|
282
|
+
}
|
|
283
|
+
else
|
|
284
|
+
take(buffer.toString('utf8', i + 1, end));
|
|
285
|
+
end = i;
|
|
286
|
+
}
|
|
287
|
+
carry = buffer.subarray(0, end);
|
|
288
|
+
}
|
|
289
|
+
if (trailing && position === floor && (!newest || floor > 0) && size > 0)
|
|
290
|
+
throw Error('preview ledger has partial record');
|
|
291
|
+
// At the file start the remaining bytes are the first complete record.
|
|
292
|
+
if (!trailing && floor === 0 && position === 0 && events.length < need)
|
|
293
|
+
take(carry.toString('utf8'));
|
|
294
|
+
}
|
|
295
|
+
finally {
|
|
296
|
+
await file.close();
|
|
297
|
+
}
|
|
298
|
+
return events;
|
|
299
|
+
}
|
|
233
300
|
export async function conversationPreview(stateDir, signal) {
|
|
234
301
|
signal.throwIfAborted();
|
|
235
302
|
const dir = join(stateDir, '.conversation');
|
|
@@ -245,30 +312,8 @@ export async function conversationPreview(stateDir, signal) {
|
|
|
245
312
|
const events = [];
|
|
246
313
|
for (const name of files) {
|
|
247
314
|
signal.throwIfAborted();
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
const size = (await file.stat()).size, length = Math.min(size, 2 * 1024 * 1024), buffer = Buffer.alloc(length);
|
|
251
|
-
await file.read(buffer, 0, length, size - length);
|
|
252
|
-
const completeEnd = buffer.lastIndexOf(10) + 1;
|
|
253
|
-
// Only the newest segment can have an in-flight append. If its tail
|
|
254
|
-
// fills the bounded window, we cannot establish the previous preview.
|
|
255
|
-
if (completeEnd < length && (name !== files[0] || (size > length && completeEnd === 0)))
|
|
256
|
-
throw Error('preview ledger has partial record');
|
|
257
|
-
const lines = buffer.toString('utf8', 0, completeEnd).split('\n');
|
|
258
|
-
lines.pop();
|
|
259
|
-
if (size > length)
|
|
260
|
-
lines.shift();
|
|
261
|
-
for (let i = lines.length - 1; i >= 0 && events.length < 200; i--) {
|
|
262
|
-
const event = JSON.parse(lines[i]);
|
|
263
|
-
if (event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq))
|
|
264
|
-
throw Error('preview ledger invalid');
|
|
265
|
-
events.push(event);
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
finally {
|
|
269
|
-
await file.close();
|
|
270
|
-
}
|
|
271
|
-
if (events.length >= 200)
|
|
315
|
+
events.push(...await previewSegmentEvents(join(dir, name), name === files[0], PREVIEW_EVENTS - events.length, signal));
|
|
316
|
+
if (events.length >= PREVIEW_EVENTS)
|
|
272
317
|
break;
|
|
273
318
|
}
|
|
274
319
|
signal.throwIfAborted();
|
|
@@ -16,6 +16,8 @@ export declare class RoleRepository {
|
|
|
16
16
|
constructor(options?: RoleRepositoryOptions);
|
|
17
17
|
list(strict?: boolean): Promise<RoleRecord[]>;
|
|
18
18
|
get(id: string): Promise<RoleRecord | undefined>;
|
|
19
|
+
private scan;
|
|
20
|
+
private record;
|
|
19
21
|
stateDir(record: RoleRecord): string | undefined;
|
|
20
22
|
private detect;
|
|
21
23
|
}
|
|
@@ -104,6 +104,20 @@ export class RoleRepository {
|
|
|
104
104
|
this.options = options;
|
|
105
105
|
}
|
|
106
106
|
async list(strict = false) {
|
|
107
|
+
const scan = this.scan(strict);
|
|
108
|
+
const names = [...new Set([...scan.configuredByName.keys(), ...scan.permanent, ...scan.temporary])].sort();
|
|
109
|
+
return boundedMap(names, this.options.concurrency ?? 8, name => this.record(scan, name, strict));
|
|
110
|
+
}
|
|
111
|
+
// One role's record is built from its own state and probe; the other roles are never probed.
|
|
112
|
+
async get(id) {
|
|
113
|
+
if (!ROLE_NAME_RE.test(id))
|
|
114
|
+
return undefined;
|
|
115
|
+
const scan = this.scan(false);
|
|
116
|
+
if (!scan.configuredByName.has(id) && !scan.permanent.has(id) && !scan.temporary.has(id))
|
|
117
|
+
return undefined;
|
|
118
|
+
return this.record(scan, id, false);
|
|
119
|
+
}
|
|
120
|
+
scan(strict) {
|
|
107
121
|
const permanentRoot = this.options.permanentRoot ?? agentsRoot();
|
|
108
122
|
const temporaryRoot = this.options.temporaryRoot ?? tmpRoot();
|
|
109
123
|
let configured = [];
|
|
@@ -116,60 +130,58 @@ export class RoleRepository {
|
|
|
116
130
|
throw error;
|
|
117
131
|
configProblem = { code: 'config_invalid', severity: 'error', source: 'config', detail: error.message };
|
|
118
132
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
const inPermanent = permanent.has(name);
|
|
126
|
-
const inTemporary = temporary.has(name);
|
|
127
|
-
const problems = configProblem ? [configProblem] : [];
|
|
128
|
-
let tempRole;
|
|
129
|
-
let stateHealth = inPermanent || inTemporary ? 'present' : 'missing';
|
|
130
|
-
if (inTemporary) {
|
|
131
|
-
const read = snapshot(join(temporaryRoot, name, 'role.yaml'), name);
|
|
132
|
-
tempRole = read.role;
|
|
133
|
-
if (strict && read.problem)
|
|
134
|
-
throw new Error('temporary role snapshot unavailable');
|
|
135
|
-
if (read.problem) {
|
|
136
|
-
problems.push(read.problem);
|
|
137
|
-
stateHealth = 'corrupt';
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (inPermanent && inTemporary)
|
|
141
|
-
problems.push({ code: 'duplicate_state', severity: 'error', detail: 'both permanent and temporary state exist' });
|
|
142
|
-
const intended = config?.session ?? tempRole?.session;
|
|
143
|
-
const detected = await this.detect(name, intended);
|
|
144
|
-
if (intended && detected !== 'none' && detected !== intended && detected !== 'ambiguous')
|
|
145
|
-
problems.push({
|
|
146
|
-
code: 'backend_mismatch', severity: 'warning',
|
|
147
|
-
detail: `configured ${intended}, detected ${detected}`,
|
|
148
|
-
});
|
|
149
|
-
const stateRef = inTemporary ? { lifetime: 'temporary' }
|
|
150
|
-
: inPermanent ? { lifetime: 'permanent' } : undefined;
|
|
151
|
-
const tempStateDir = inTemporary ? join(temporaryRoot, name) : undefined;
|
|
152
|
-
const taskId = !config && tempStateDir ? retainedTaskId(tempStateDir, name) : undefined;
|
|
153
|
-
return {
|
|
154
|
-
id: name,
|
|
155
|
-
lifetime: config ? 'permanent' : inTemporary ? 'temporary' : 'orphan',
|
|
156
|
-
configured: Boolean(config),
|
|
157
|
-
...(taskId ? { taskId } : {}),
|
|
158
|
-
config: config ? view(config) : tempRole ? view(tempRole) : undefined,
|
|
159
|
-
stateRef,
|
|
160
|
-
stateHealth,
|
|
161
|
-
configuredBackend: intended,
|
|
162
|
-
detectedBackend: detected,
|
|
163
|
-
compatibility: { compatible: true },
|
|
164
|
-
problems,
|
|
165
|
-
lineage: tempStateDir ? readSpawnLineage(tempStateDir) : undefined,
|
|
166
|
-
};
|
|
167
|
-
});
|
|
133
|
+
return {
|
|
134
|
+
temporaryRoot, configProblem,
|
|
135
|
+
permanent: new Set(safeDirs(permanentRoot, strict)),
|
|
136
|
+
temporary: new Set(safeDirs(temporaryRoot, strict)),
|
|
137
|
+
configuredByName: new Map(configured.map(role => [role.name, role])),
|
|
138
|
+
};
|
|
168
139
|
}
|
|
169
|
-
async
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
140
|
+
async record(scan, name, strict) {
|
|
141
|
+
const { temporaryRoot, configProblem } = scan;
|
|
142
|
+
const config = scan.configuredByName.get(name);
|
|
143
|
+
const inPermanent = scan.permanent.has(name);
|
|
144
|
+
const inTemporary = scan.temporary.has(name);
|
|
145
|
+
const problems = configProblem ? [configProblem] : [];
|
|
146
|
+
let tempRole;
|
|
147
|
+
let stateHealth = inPermanent || inTemporary ? 'present' : 'missing';
|
|
148
|
+
if (inTemporary) {
|
|
149
|
+
const read = snapshot(join(temporaryRoot, name, 'role.yaml'), name);
|
|
150
|
+
tempRole = read.role;
|
|
151
|
+
if (strict && read.problem)
|
|
152
|
+
throw new Error('temporary role snapshot unavailable');
|
|
153
|
+
if (read.problem) {
|
|
154
|
+
problems.push(read.problem);
|
|
155
|
+
stateHealth = 'corrupt';
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
if (inPermanent && inTemporary)
|
|
159
|
+
problems.push({ code: 'duplicate_state', severity: 'error', detail: 'both permanent and temporary state exist' });
|
|
160
|
+
const intended = config?.session ?? tempRole?.session;
|
|
161
|
+
const detected = await this.detect(name, intended);
|
|
162
|
+
if (intended && detected !== 'none' && detected !== intended && detected !== 'ambiguous')
|
|
163
|
+
problems.push({
|
|
164
|
+
code: 'backend_mismatch', severity: 'warning',
|
|
165
|
+
detail: `configured ${intended}, detected ${detected}`,
|
|
166
|
+
});
|
|
167
|
+
const stateRef = inTemporary ? { lifetime: 'temporary' }
|
|
168
|
+
: inPermanent ? { lifetime: 'permanent' } : undefined;
|
|
169
|
+
const tempStateDir = inTemporary ? join(temporaryRoot, name) : undefined;
|
|
170
|
+
const taskId = !config && tempStateDir ? retainedTaskId(tempStateDir, name) : undefined;
|
|
171
|
+
return {
|
|
172
|
+
id: name,
|
|
173
|
+
lifetime: config ? 'permanent' : inTemporary ? 'temporary' : 'orphan',
|
|
174
|
+
configured: Boolean(config),
|
|
175
|
+
...(taskId ? { taskId } : {}),
|
|
176
|
+
config: config ? view(config) : tempRole ? view(tempRole) : undefined,
|
|
177
|
+
stateRef,
|
|
178
|
+
stateHealth,
|
|
179
|
+
configuredBackend: intended,
|
|
180
|
+
detectedBackend: detected,
|
|
181
|
+
compatibility: { compatible: true },
|
|
182
|
+
problems,
|
|
183
|
+
lineage: tempStateDir ? readSpawnLineage(tempStateDir) : undefined,
|
|
184
|
+
};
|
|
173
185
|
}
|
|
174
186
|
stateDir(record) {
|
|
175
187
|
if (!record.stateRef)
|
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.84",
|
|
3
|
+
"buildId": "a63a3595510b",
|
|
4
|
+
"commit": "6db38803d52345eada7d87cc364a8a98d1144666",
|
|
5
5
|
"dirty": true,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-20T19:57:42.666Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
|
@@ -62,6 +62,8 @@ export interface ResourceWireFrame {
|
|
|
62
62
|
export declare const RESOURCE_FRAME_BYTES: number;
|
|
63
63
|
export declare const RESOURCE_PENDING_BYTES: number;
|
|
64
64
|
export declare const RESOURCE_ASSEMBLY_BYTES: number;
|
|
65
|
+
/** Minimum spacing of activity-only follow-up captures of one view. */
|
|
66
|
+
export declare const ACTIVITY_CAPTURE_INTERVAL_MS = 1000;
|
|
65
67
|
/** Lazily yield byte chunks so backpressure never queues an entire large record.
|
|
66
68
|
* Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
|
|
67
69
|
export declare function resourceFrames(envelope: ResourceEnvelope): Generator<ResourceWireFrame>;
|
|
@@ -109,9 +111,10 @@ export declare class FleetResourceViews {
|
|
|
109
111
|
private scope;
|
|
110
112
|
private project;
|
|
111
113
|
private configurationInputs;
|
|
114
|
+
private activityCaptureIntervalMs;
|
|
112
115
|
readonly producerGeneration: `${string}-${string}-${string}-${string}-${string}`;
|
|
113
116
|
private connections;
|
|
114
|
-
constructor(scope: FleetResourceScope, project: (descriptor: ResourceDescriptor, signal: AbortSignal, owner: string) => Promise<unknown>, configurationInputs?: () => readonly string[]);
|
|
117
|
+
constructor(scope: FleetResourceScope, project: (descriptor: ResourceDescriptor, signal: AbortSignal, owner: string) => Promise<unknown>, configurationInputs?: () => readonly string[], activityCaptureIntervalMs?: number);
|
|
115
118
|
open(response: ServerResponse, owner: string, authorized: () => boolean): {
|
|
116
119
|
connectionId: `${string}-${string}-${string}-${string}-${string}`;
|
|
117
120
|
close: () => void;
|
|
@@ -23,6 +23,8 @@ export const RESOURCE_FRAME_BYTES = 512 * 1024;
|
|
|
23
23
|
export const RESOURCE_PENDING_BYTES = 8 * 1024 * 1024;
|
|
24
24
|
export const RESOURCE_ASSEMBLY_BYTES = 16 * 1024 * 1024;
|
|
25
25
|
const CHUNK_BYTES = 256 * 1024;
|
|
26
|
+
/** Minimum spacing of activity-only follow-up captures of one view. */
|
|
27
|
+
export const ACTIVITY_CAPTURE_INTERVAL_MS = 1000;
|
|
26
28
|
/** Lazily yield byte chunks so backpressure never queues an entire large record.
|
|
27
29
|
* Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
|
|
28
30
|
export function* resourceFrames(envelope) {
|
|
@@ -187,12 +189,14 @@ export class FleetResourceViews {
|
|
|
187
189
|
scope;
|
|
188
190
|
project;
|
|
189
191
|
configurationInputs;
|
|
192
|
+
activityCaptureIntervalMs;
|
|
190
193
|
producerGeneration = randomUUID();
|
|
191
194
|
connections = new Map();
|
|
192
|
-
constructor(scope, project, configurationInputs = () => []) {
|
|
195
|
+
constructor(scope, project, configurationInputs = () => [], activityCaptureIntervalMs = ACTIVITY_CAPTURE_INTERVAL_MS) {
|
|
193
196
|
this.scope = scope;
|
|
194
197
|
this.project = project;
|
|
195
198
|
this.configurationInputs = configurationInputs;
|
|
199
|
+
this.activityCaptureIntervalMs = activityCaptureIntervalMs;
|
|
196
200
|
}
|
|
197
201
|
open(response, owner, authorized) {
|
|
198
202
|
const id = randomUUID(), controller = new AbortController();
|
|
@@ -334,7 +338,7 @@ export class FleetResourceViews {
|
|
|
334
338
|
return connection;
|
|
335
339
|
}
|
|
336
340
|
current(connection, view) { return !connection.controller.signal.aborted && !view.controller.signal.aborted && connection.views.get(view.descriptor.viewId) === view; }
|
|
337
|
-
retire(view) { view.controller.abort(); view.rejectInitial(new ResourceBindingError(409, 'resource binding retired')); }
|
|
341
|
+
retire(view) { clearTimeout(view.activityTimer); view.activityTimer = undefined; view.controller.abort(); view.rejectInitial(new ResourceBindingError(409, 'resource binding retired')); }
|
|
338
342
|
emit(connection, view, kind, payload, source) {
|
|
339
343
|
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')
|
|
340
344
|
view.qualifiedAt = Date.now(); });
|
|
@@ -388,10 +392,10 @@ export class FleetResourceViews {
|
|
|
388
392
|
return false;
|
|
389
393
|
// Only classify known activity writes under the configured role roots.
|
|
390
394
|
// Parent/unknown events and every other file retain full invalidation.
|
|
391
|
-
// The canonical web producer atomically replaces hashed checkpoint files
|
|
392
|
-
//
|
|
393
|
-
//
|
|
394
|
-
if (
|
|
395
|
+
// The canonical web producer atomically replaces hashed checkpoint files,
|
|
396
|
+
// once per ledger advance of any role. No projection reads these delivery
|
|
397
|
+
// acknowledgements, so they neither fence a capture nor wake a follow-up.
|
|
398
|
+
if (/^[a-f0-9]{64}\.json$/.test(relative(resolve(stateRoot(), 'web', 'notification-outboxes'), path)))
|
|
395
399
|
return false;
|
|
396
400
|
for (const root of roots) {
|
|
397
401
|
const parts = relative(resolve(root), path).split(sep);
|
|
@@ -421,9 +425,28 @@ export class FleetResourceViews {
|
|
|
421
425
|
}
|
|
422
426
|
return true;
|
|
423
427
|
}
|
|
424
|
-
schedule(connection, view) {
|
|
428
|
+
schedule(connection, view, due = false) {
|
|
425
429
|
if (!this.current(connection, view) || view.failed)
|
|
426
430
|
return;
|
|
431
|
+
// Agent activity (ledger appends, status files) changes previews and status
|
|
432
|
+
// continuously while an agent works. Without a structural change since the
|
|
433
|
+
// latest capture began, coalesce those wakes into one trailing capture per
|
|
434
|
+
// interval instead of re-projecting every role after every append.
|
|
435
|
+
const wait = !due && view.captureSource !== view.source && view.capturedAt !== undefined
|
|
436
|
+
&& view.captureSource.snapshot().position === view.capturedStructure
|
|
437
|
+
? this.activityCaptureIntervalMs - (Date.now() - view.capturedAt) : 0;
|
|
438
|
+
if (wait > 0) {
|
|
439
|
+
view.scheduledPosition = view.source.snapshot().position;
|
|
440
|
+
view.dirty = true;
|
|
441
|
+
if (!view.running && !view.activityTimer) {
|
|
442
|
+
// The trailing capture runs when its timer fires, whatever the clock reads.
|
|
443
|
+
view.activityTimer = setTimeout(() => { view.activityTimer = undefined; this.schedule(connection, view, true); }, Math.min(wait, this.activityCaptureIntervalMs));
|
|
444
|
+
view.activityTimer.unref?.();
|
|
445
|
+
}
|
|
446
|
+
return;
|
|
447
|
+
}
|
|
448
|
+
clearTimeout(view.activityTimer);
|
|
449
|
+
view.activityTimer = undefined;
|
|
427
450
|
view.scheduledPosition = view.source.snapshot().position;
|
|
428
451
|
view.dirty = true;
|
|
429
452
|
if (view.running)
|
|
@@ -446,6 +469,8 @@ export class FleetResourceViews {
|
|
|
446
469
|
if (!view.dirty || !this.current(connection, view))
|
|
447
470
|
return;
|
|
448
471
|
view.dirty = false;
|
|
472
|
+
view.capturedAt = Date.now();
|
|
473
|
+
view.capturedStructure = view.captureSource.snapshot().position;
|
|
449
474
|
let payload, source;
|
|
450
475
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
451
476
|
view.controller.signal.throwIfAborted();
|
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.84",
|
|
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",
|