@ours.network/fleet 2.2.2 → 2.2.3
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.
|
@@ -7,39 +7,155 @@ const HISTORICAL_KINDS = ['file.attached', 'fleet.task_created', 'prompt.admitte
|
|
|
7
7
|
export function conversationEventVisible(event, sessionGeneration, resumedSessionId) {
|
|
8
8
|
return event.source !== 'agent_replay' && (event.sessionGeneration === sessionGeneration || (!!resumedSessionId && HISTORICAL_KINDS.includes(event.kind) && event.acpSessionId === resumedSessionId));
|
|
9
9
|
}
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
// A seek attempt is transactional: its caller discards all collected rows and
|
|
11
|
+
// performs one ordinary scan if a probe or the file identity is no longer safe.
|
|
12
|
+
class SeekFallback extends Error {
|
|
13
|
+
}
|
|
14
|
+
const stamp = (info) => `${info.dev}:${info.ino}:${info.size}:${info.mtimeMs}:${info.ctimeMs}`;
|
|
15
|
+
/** Strict lower bound in an admitted monotonic writer segment, on this FD only. */
|
|
16
|
+
async function seekBefore(file, size, before, signal) {
|
|
17
|
+
const read = async (position, length) => {
|
|
18
|
+
signal?.throwIfAborted();
|
|
19
|
+
const bytes = Buffer.alloc(length);
|
|
20
|
+
const { bytesRead } = await file.read(bytes, 0, length, position);
|
|
21
|
+
signal?.throwIfAborted();
|
|
22
|
+
if (bytesRead !== length)
|
|
23
|
+
throw new SeekFallback('segment changed during seek');
|
|
24
|
+
return bytes;
|
|
25
|
+
};
|
|
26
|
+
if (!size)
|
|
27
|
+
return 0;
|
|
28
|
+
// An active partial append is not a safe boundary, even if the first event
|
|
29
|
+
// would let us skip this whole segment.
|
|
30
|
+
if ((await read(size - 1, 1))[0] !== 10)
|
|
31
|
+
throw new SeekFallback('partial segment');
|
|
32
|
+
const record = async (position) => {
|
|
33
|
+
let start = position, end = position;
|
|
34
|
+
const left = [], right = [];
|
|
35
|
+
while (start > 0) {
|
|
36
|
+
const length = Math.min(4096, start), chunk = await read(start - length, length);
|
|
37
|
+
const newline = chunk.lastIndexOf(10);
|
|
38
|
+
left.push(chunk.subarray(newline + 1));
|
|
39
|
+
start -= length;
|
|
40
|
+
if (newline >= 0) {
|
|
41
|
+
start += newline + 1;
|
|
42
|
+
break;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
while (end < size) {
|
|
46
|
+
const chunk = await read(end, Math.min(4096, size - end)), newline = chunk.indexOf(10);
|
|
47
|
+
right.push(newline < 0 ? chunk : chunk.subarray(0, newline));
|
|
48
|
+
end += newline < 0 ? chunk.length : newline + 1;
|
|
49
|
+
if (newline >= 0)
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
let event;
|
|
53
|
+
try {
|
|
54
|
+
event = JSON.parse(Buffer.concat([...left.reverse(), ...right]).toString('utf8'));
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
throw new SeekFallback('unreadable seek record');
|
|
58
|
+
}
|
|
59
|
+
if (!event || event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq) || event.seq <= 0)
|
|
60
|
+
throw new SeekFallback('legacy seek record');
|
|
61
|
+
return { start, end, seq: event.seq };
|
|
62
|
+
};
|
|
63
|
+
const first = await record(0);
|
|
64
|
+
if (first.seq >= before)
|
|
65
|
+
return 0;
|
|
66
|
+
let low = first.end, high = size, lower = first.seq, upper = Infinity;
|
|
67
|
+
while (low < high) {
|
|
68
|
+
const found = await record(Math.floor((low + high) / 2));
|
|
69
|
+
if (found.start < low || found.end > high || found.seq <= lower || found.seq >= upper)
|
|
70
|
+
throw new SeekFallback('non-monotonic seek record');
|
|
71
|
+
if (found.seq < before) {
|
|
72
|
+
low = found.end;
|
|
73
|
+
lower = found.seq;
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
high = found.start;
|
|
77
|
+
upper = found.seq;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return low;
|
|
81
|
+
}
|
|
82
|
+
/** Newest-first lines; optional finite-before seek is only for a healthy writer. */
|
|
83
|
+
async function readLinesBackward(path, visit, signal, seek) {
|
|
12
84
|
signal?.throwIfAborted();
|
|
13
|
-
|
|
85
|
+
let file;
|
|
86
|
+
try {
|
|
87
|
+
file = await open(path, 'r');
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
signal?.throwIfAborted();
|
|
91
|
+
if (seek)
|
|
92
|
+
throw new SeekFallback('segment disappeared');
|
|
93
|
+
throw error;
|
|
94
|
+
}
|
|
14
95
|
try {
|
|
15
|
-
|
|
16
|
-
let
|
|
17
|
-
|
|
96
|
+
const initial = await file.stat(), key = stamp(initial);
|
|
97
|
+
let position = seek ? await seekBefore(file, initial.size, seek.before, signal) : initial.size;
|
|
98
|
+
let carry = Buffer.alloc(0), more = true;
|
|
99
|
+
scan: while (position > 0) {
|
|
18
100
|
signal?.throwIfAborted();
|
|
19
101
|
const size = Math.min(256 * 1024, position);
|
|
20
102
|
position -= size;
|
|
21
103
|
const chunk = Buffer.alloc(size);
|
|
22
|
-
await file.read(chunk, 0, size, position);
|
|
104
|
+
const { bytesRead } = await file.read(chunk, 0, size, position);
|
|
23
105
|
signal?.throwIfAborted();
|
|
24
|
-
|
|
106
|
+
if (seek && bytesRead !== size)
|
|
107
|
+
throw new SeekFallback('short segment read');
|
|
108
|
+
const buffer = Buffer.concat([chunk.subarray(0, bytesRead), carry]);
|
|
25
109
|
let end = buffer.length;
|
|
26
110
|
for (let i = buffer.length - 1; i >= 0; i--) {
|
|
27
111
|
if (buffer[i] !== 10)
|
|
28
112
|
continue;
|
|
29
|
-
if (end > i + 1 && !visit(buffer.toString('utf8', i + 1, end)))
|
|
30
|
-
|
|
113
|
+
if (end > i + 1 && !visit(buffer.toString('utf8', i + 1, end))) {
|
|
114
|
+
more = false;
|
|
115
|
+
break scan;
|
|
116
|
+
}
|
|
31
117
|
end = i;
|
|
32
118
|
}
|
|
33
119
|
carry = buffer.subarray(0, end);
|
|
34
120
|
}
|
|
35
|
-
if (carry.length && !visit(carry.toString('utf8')))
|
|
36
|
-
|
|
37
|
-
|
|
121
|
+
if (more && carry.length && !visit(carry.toString('utf8')))
|
|
122
|
+
more = false;
|
|
123
|
+
if (seek) {
|
|
124
|
+
signal?.throwIfAborted();
|
|
125
|
+
const current = await stat(path).catch(() => undefined);
|
|
126
|
+
if (stamp(await file.stat()) !== key || !current || stamp(current) !== key)
|
|
127
|
+
throw new SeekFallback('segment changed during read');
|
|
128
|
+
seek.checked.push({ path, key });
|
|
129
|
+
}
|
|
130
|
+
return more;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
signal?.throwIfAborted();
|
|
134
|
+
if (seek && !(error instanceof SeekFallback))
|
|
135
|
+
throw new SeekFallback('seek read unavailable');
|
|
136
|
+
throw error;
|
|
38
137
|
}
|
|
39
138
|
finally {
|
|
40
139
|
await file.close();
|
|
41
140
|
}
|
|
42
141
|
}
|
|
142
|
+
/** Existing writer provenance, never a substitute for the captured barrier. */
|
|
143
|
+
async function seekManifest(dir, files, signal) {
|
|
144
|
+
try {
|
|
145
|
+
signal?.throwIfAborted();
|
|
146
|
+
const raw = await readFile(join(dir, 'manifest.json'), { encoding: 'utf8', signal }), value = JSON.parse(raw);
|
|
147
|
+
if (value.schemaVersion !== 1 || !Number.isSafeInteger(value.nextSeq) || value.nextSeq < 1
|
|
148
|
+
|| !Array.isArray(value.segments) || value.segments.length !== files.length
|
|
149
|
+
|| value.segments.some((name, i) => name !== files[files.length - 1 - i])
|
|
150
|
+
|| (value.failedGenerations !== undefined && (!Array.isArray(value.failedGenerations) || value.failedGenerations.length > 0)))
|
|
151
|
+
return;
|
|
152
|
+
return raw;
|
|
153
|
+
}
|
|
154
|
+
catch {
|
|
155
|
+
signal?.throwIfAborted();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
43
159
|
// Rotated segments never change, so their last cursor is reused until the file itself changes.
|
|
44
160
|
const lastSeqs = new Map();
|
|
45
161
|
/** The last event cursor in a segment, read from its tail; undefined when none is readable. */
|
|
@@ -72,6 +188,17 @@ async function lastSeq(path) {
|
|
|
72
188
|
* `nextCursor` is the newest ledger position seen, for forward `after` paging.
|
|
73
189
|
*/
|
|
74
190
|
export async function conversationTailPage(stateDir, request, snapshot, resumedSessionId, signal) {
|
|
191
|
+
try {
|
|
192
|
+
return await tailPage(stateDir, request, snapshot, resumedSessionId, signal, true);
|
|
193
|
+
}
|
|
194
|
+
catch (error) {
|
|
195
|
+
signal?.throwIfAborted();
|
|
196
|
+
if (!(error instanceof SeekFallback))
|
|
197
|
+
throw error;
|
|
198
|
+
return tailPage(stateDir, request, snapshot, resumedSessionId, signal, false);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async function tailPage(stateDir, request, snapshot, resumedSessionId, signal, allowSeek) {
|
|
75
202
|
signal?.throwIfAborted();
|
|
76
203
|
const before = request.before === undefined ? Infinity : Number(request.before);
|
|
77
204
|
if (before !== Infinity && (!Number.isSafeInteger(before) || before < 0))
|
|
@@ -93,6 +220,11 @@ export async function conversationTailPage(stateDir, request, snapshot, resumedS
|
|
|
93
220
|
// generation on every open. Legacy/incomplete snapshots still need context.
|
|
94
221
|
const complete = !snapshot.observationFailure && Number.isSafeInteger(barrier) && barrier >= 0
|
|
95
222
|
&& Number.isSafeInteger(observation.stateRevision) && observation.stateRevision >= 0;
|
|
223
|
+
const dir = join(stateDir, '.conversation');
|
|
224
|
+
const manifest = allowSeek && before !== Infinity && complete && !snapshot.historyDegraded
|
|
225
|
+
? await seekManifest(dir, files, signal) : undefined;
|
|
226
|
+
const seek = manifest === undefined ? undefined : { before, checked: [] };
|
|
227
|
+
let previousSeq = Infinity;
|
|
96
228
|
const needsCapabilities = !complete || !Array.isArray(observation.commands);
|
|
97
229
|
const pending = new Set(complete && Array.isArray(observation.pendingPermissions)
|
|
98
230
|
&& snapshot.pendingPermissionIds.every(id => observation.pendingPermissions.some(permission => permission.permissionId === id))
|
|
@@ -117,9 +249,16 @@ export async function conversationTailPage(stateDir, request, snapshot, resumedS
|
|
|
117
249
|
event = JSON.parse(line);
|
|
118
250
|
}
|
|
119
251
|
catch {
|
|
252
|
+
if (seek)
|
|
253
|
+
throw new SeekFallback('unreadable page record');
|
|
120
254
|
return true;
|
|
121
255
|
}
|
|
122
256
|
const seq = Number(event.seq);
|
|
257
|
+
if (seek) {
|
|
258
|
+
if (event.schemaVersion !== 1 || !Number.isSafeInteger(seq) || seq <= 0 || seq >= previousSeq)
|
|
259
|
+
throw new SeekFallback('non-monotonic page record');
|
|
260
|
+
previousSeq = seq;
|
|
261
|
+
}
|
|
123
262
|
if (!Number.isSafeInteger(seq))
|
|
124
263
|
return true;
|
|
125
264
|
if (barrier !== undefined && seq > barrier)
|
|
@@ -142,10 +281,22 @@ export async function conversationTailPage(stateDir, request, snapshot, resumedS
|
|
|
142
281
|
hasOlder = true;
|
|
143
282
|
}
|
|
144
283
|
return !hasOlder || wantsContext();
|
|
145
|
-
}, signal);
|
|
284
|
+
}, signal, seek);
|
|
146
285
|
if (!more)
|
|
147
286
|
break;
|
|
148
287
|
}
|
|
288
|
+
if (seek) {
|
|
289
|
+
signal?.throwIfAborted();
|
|
290
|
+
const currentFiles = (await readdir(dir).catch(() => { throw new SeekFallback('ledger disappeared'); })).filter(n => /^events-\d{6}\.jsonl$/.test(n)).sort().reverse();
|
|
291
|
+
if (await seekManifest(dir, currentFiles, signal) !== manifest || currentFiles.join() !== files.join())
|
|
292
|
+
throw new SeekFallback('segments rotated during page');
|
|
293
|
+
for (const checked of seek.checked) {
|
|
294
|
+
signal?.throwIfAborted();
|
|
295
|
+
const current = await stat(checked.path).catch(() => undefined);
|
|
296
|
+
if (!current || stamp(current) !== checked.key)
|
|
297
|
+
throw new SeekFallback('segment replaced during page');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
149
300
|
events.reverse();
|
|
150
301
|
context.reverse();
|
|
151
302
|
return { events, snapshot, hasOlder, context, hasMore: false,
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "2.2.3",
|
|
3
|
+
"buildId": "3ac91b025945",
|
|
4
|
+
"commit": "576470bf1cd528fb2d517d081358b3e5c667eea3",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-10-
|
|
6
|
+
"builtAt": "2026-10-29T08:13:26.500Z",
|
|
7
7
|
"capabilities": [
|
|
8
8
|
"service.refresh-v1",
|
|
9
9
|
"managed-cli.codex-reasoning-effort-v1",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ours.network/fleet",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.3",
|
|
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",
|