@ours.network/fleet 1.2.0-nightly.77 → 1.2.0-nightly.79

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.
@@ -17,7 +17,7 @@ export interface ConversationTailPage extends ConversationPageView {
17
17
  export declare function conversationTailPage(stateDir: string, request: {
18
18
  before?: string;
19
19
  limit?: number;
20
- }, snapshot: ConversationPageView['snapshot'], resumedSessionId?: string): Promise<ConversationTailPage>;
20
+ }, snapshot: ConversationPageView['snapshot'], resumedSessionId?: string, signal?: AbortSignal): Promise<ConversationTailPage>;
21
21
  /** Read the existing ACP session ledger without opening its writer/recovery store. */
22
22
  export declare function resumedConversationPage(stateDir: string, request: {
23
23
  after?: string;
@@ -8,16 +8,19 @@ export function conversationEventVisible(event, sessionGeneration, resumedSessio
8
8
  return event.source !== 'agent_replay' && (event.sessionGeneration === sessionGeneration || (!!resumedSessionId && HISTORICAL_KINDS.includes(event.kind) && event.acpSessionId === resumedSessionId));
9
9
  }
10
10
  /** Newest-first lines of a file, read in chunks from its end; `visit` returns false to stop. */
11
- async function readLinesBackward(path, visit) {
11
+ async function readLinesBackward(path, visit, signal) {
12
+ signal?.throwIfAborted();
12
13
  const file = await open(path, 'r');
13
14
  try {
14
15
  let position = (await file.stat()).size;
15
16
  let carry = Buffer.alloc(0);
16
17
  while (position > 0) {
18
+ signal?.throwIfAborted();
17
19
  const size = Math.min(256 * 1024, position);
18
20
  position -= size;
19
21
  const chunk = Buffer.alloc(size);
20
22
  await file.read(chunk, 0, size, position);
23
+ signal?.throwIfAborted();
21
24
  let buffer = Buffer.concat([chunk, carry]);
22
25
  let end = buffer.length;
23
26
  for (let i = buffer.length - 1; i >= 0; i--) {
@@ -55,7 +58,8 @@ async function lastSeq(path) {
55
58
  * read backwards from the ledger so opening a long session costs one page.
56
59
  * `nextCursor` is the newest ledger position seen, for forward `after` paging.
57
60
  */
58
- export async function conversationTailPage(stateDir, request, snapshot, resumedSessionId) {
61
+ export async function conversationTailPage(stateDir, request, snapshot, resumedSessionId, signal) {
62
+ signal?.throwIfAborted();
59
63
  const before = request.before === undefined ? Infinity : Number(request.before);
60
64
  if (before !== Infinity && (!Number.isSafeInteger(before) || before < 0))
61
65
  throw new Error('before must be a ledger cursor');
@@ -68,12 +72,22 @@ export async function conversationTailPage(stateDir, request, snapshot, resumedS
68
72
  const events = [];
69
73
  let newest;
70
74
  let hasOlder = false;
71
- const barrier = snapshot.ledgerBarrier;
72
- const pending = new Set(snapshot.pendingPermissionIds);
75
+ const observation = snapshot;
76
+ const barrier = observation.ledgerBarrier;
77
+ // Current supervisors capture these sanitized inventories together with the
78
+ // ledger barrier. The browser uses them directly, including authoritative
79
+ // empty arrays. Looking for their old ledger events would scan the entire
80
+ // generation on every open. Legacy/incomplete snapshots still need context.
81
+ const complete = !snapshot.observationFailure && Number.isSafeInteger(barrier) && barrier >= 0
82
+ && Number.isSafeInteger(observation.stateRevision) && observation.stateRevision >= 0;
83
+ const needsCapabilities = !complete || !Array.isArray(observation.commands);
84
+ const pending = new Set(complete && Array.isArray(observation.pendingPermissions)
85
+ && snapshot.pendingPermissionIds.every(id => observation.pendingPermissions.some(permission => permission.permissionId === id))
86
+ ? [] : snapshot.pendingPermissionIds);
73
87
  let capabilities;
74
88
  const context = [];
75
89
  // Context only accompanies the newest page; older pages stop as soon as they are full.
76
- const wantsContext = () => before === Infinity && (!capabilities || pending.size > 0);
90
+ const wantsContext = () => before === Infinity && ((needsCapabilities && !capabilities) || pending.size > 0);
77
91
  const generation = `"sessionGeneration":${JSON.stringify(snapshot.sessionGeneration)}`;
78
92
  for (const file of files) {
79
93
  const more = await readLinesBackward(join(stateDir, '.conversation', file), line => {
@@ -115,7 +129,7 @@ export async function conversationTailPage(stateDir, request, snapshot, resumedS
115
129
  hasOlder = true;
116
130
  }
117
131
  return !hasOlder || wantsContext();
118
- });
132
+ }, signal);
119
133
  if (!more)
120
134
  break;
121
135
  }
@@ -70,6 +70,7 @@ export interface RoleSessionControl {
70
70
  conversationTail?(request: {
71
71
  before?: string;
72
72
  limit?: number;
73
+ signal?: AbortSignal;
73
74
  }): Promise<ConversationTailPage>;
74
75
  submitPromptV2?(request: {
75
76
  commandId: string;
@@ -128,6 +129,7 @@ export declare class RoleSessionControlAdapter implements RoleSessionControl {
128
129
  conversationTail(request?: {
129
130
  before?: string;
130
131
  limit?: number;
132
+ signal?: AbortSignal;
131
133
  }): Promise<ConversationTailPage>;
132
134
  submitPromptV2(request: {
133
135
  commandId: string;
@@ -98,7 +98,9 @@ export class RoleSessionControlAdapter {
98
98
  async conversationTail(request = {}) {
99
99
  // A replacement while reading disk invalidates both the tail and its snapshot.
100
100
  for (let attempt = 0; attempt < 3; attempt++) {
101
+ request.signal?.throwIfAborted();
101
102
  const live = await this.call('conversation_page', { limit: 1 });
103
+ request.signal?.throwIfAborted();
102
104
  const snapshot = await this.snapshot();
103
105
  let resumed;
104
106
  if (snapshot.backend === 'acp' && snapshot.sessionId) {
@@ -110,12 +112,15 @@ export class RoleSessionControlAdapter {
110
112
  }
111
113
  let page;
112
114
  try {
113
- page = await conversationTailPage(this.stateDir, request, live.snapshot, resumed);
115
+ page = await conversationTailPage(this.stateDir, request, live.snapshot, resumed, request.signal);
114
116
  }
115
117
  catch (error) {
118
+ request.signal?.throwIfAborted();
116
119
  throw new FleetError('invalid_request', error.message);
117
120
  }
121
+ request.signal?.throwIfAborted();
118
122
  const check = await this.call('conversation_page', { limit: 1 });
123
+ request.signal?.throwIfAborted();
119
124
  if (check.snapshot.sessionGeneration === live.snapshot.sessionGeneration)
120
125
  return page;
121
126
  }
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.77",
3
- "buildId": "bba4bf03b747",
4
- "commit": "9bdfe660ab6660b0aa75f2d2bc84254e2e811373",
2
+ "version": "1.2.0-nightly.79",
3
+ "buildId": "ed6b63d22064",
4
+ "commit": "62a21f66895ff79cd5e5ea190febcfc59611e946",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-19T20:21:21.233Z",
6
+ "builtAt": "2026-10-19T21:53:54.074Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -504,6 +504,13 @@ export class ConversationEventStore {
504
504
  return this.tail.filter(event => event.seq > after).slice(0, limit);
505
505
  const events = [];
506
506
  for (const segment of this.segments) {
507
+ // Recovery/append already indexed these single-writer segments. A page
508
+ // after a resumed generation's floor must not reread every older segment
509
+ // merely because the requested cursor is outside the memory tail.
510
+ // Degraded/unknown ranges retain the conservative recovery read path.
511
+ const range = this.segmentRanges.get(segment);
512
+ if (!this._degraded && range && !range.damaged && range.last !== undefined && range.last <= after)
513
+ continue;
507
514
  for (const event of this.readSegment(segment)) {
508
515
  if (event.seq <= after)
509
516
  continue;
@@ -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();
@@ -351,6 +352,14 @@ export class FleetResourceViews {
351
352
  // otherwise reserved for an independent task receipt.
352
353
  if (this.scope.configPath && resolve(path) === resolve(this.scope.configPath))
353
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;
354
363
  // Preview selection reads role config/state and conversation ledgers, never
355
364
  // task layout run/operation/brief receipts. A worker may commit several of
356
365
  // these while that independent canonical preview read is in flight.
@@ -364,7 +373,6 @@ export class FleetResourceViews {
364
373
  // retain full wakes without requiring an outbox-quiet capture window.
365
374
  if (structural && /^[a-f0-9]{64}\.json$/.test(relative(resolve(stateRoot(), 'web', 'notification-outboxes'), path)))
366
375
  return false;
367
- const roots = this.scope.roots ?? [agentsRoot(), tmpRoot()];
368
376
  for (const root of roots) {
369
377
  const parts = relative(resolve(root), path).split(sep);
370
378
  if (parts[0] === '..')
@@ -925,7 +925,7 @@ export async function buildWebServer(services, boundary, options = {}) {
925
925
  limit: request.query.limit ? Number(request.query.limit) : undefined,
926
926
  });
927
927
  });
928
- app.get('/api/v1/roles/:id/conversation', async (request) => {
928
+ app.get('/api/v1/roles/:id/conversation', async (request, reply) => {
929
929
  auth.authenticate(request);
930
930
  const control = await services.session(request.params.id);
931
931
  if (!control.conversationPage)
@@ -941,8 +941,21 @@ export async function buildWebServer(services, boundary, options = {}) {
941
941
  const stateDir = role ? services.repository.stateDir(role) : undefined;
942
942
  const present = (event) => presentAttachmentEvent(stateDir, request.params.id, event);
943
943
  if (backward) {
944
- const page = await control.conversationTail({ before: request.query.before, limit });
945
- return { ...page, events: page.events.map(present), context: page.context.map(present) };
944
+ const owner = new AbortController();
945
+ const cancel = () => owner.abort();
946
+ request.raw.once('aborted', cancel);
947
+ reply.raw.once('close', cancel);
948
+ if (request.raw.aborted || reply.raw.destroyed)
949
+ cancel();
950
+ try {
951
+ const page = await control.conversationTail({ before: request.query.before, limit, signal: owner.signal });
952
+ owner.signal.throwIfAborted();
953
+ return { ...page, events: page.events.map(present), context: page.context.map(present) };
954
+ }
955
+ finally {
956
+ request.raw.off('aborted', cancel);
957
+ reply.raw.off('close', cancel);
958
+ }
946
959
  }
947
960
  const page = await control.conversationPage({ after: request.query.after, limit });
948
961
  return { ...page, events: page.events.map(present) };
@@ -1,5 +1,25 @@
1
1
  # Conversation event stream (wire version 1)
2
2
 
3
+ Initial `conversation?latest=1&limit=200` history uses the captured command and
4
+ pending-permission inventories when the supervisor supplies a complete observation
5
+ snapshot with its revision and ledger barrier. Empty inventories are authoritative.
6
+ This avoids scanning the current generation for old context events after the
7
+ latest page is full. Legacy, failed or incomplete captures retain ledger context
8
+ recovery. Generation checks, resumed-session filtering and event cursors still apply.
9
+ The private snapshot checks also use the store's recovered/appended segment ranges
10
+ to skip healthy segments ending at or before their cursor. A retained prior
11
+ generation therefore does not require rereading its whole prefix merely because
12
+ the current-generation cursor is outside the in-memory tail. Degraded stores and
13
+ unknown or damaged ranges retain conservative reads.
14
+ Older pages can scan intervening newer ledger segments; the event count alone is
15
+ not a disk-read byte bound.
16
+
17
+ Disconnecting a latest/before HTTP history request cancels its disk tail reader
18
+ at asynchronous read boundaries and prevents further orchestration or publication.
19
+ An already admitted private-control RPC retains its existing timeout; disconnect
20
+ does not interrupt the agent or its work. Forward-history reads and passive SSE
21
+ retain their separate lifetimes.
22
+
3
23
  `GET /api/v1/roles/:id/conversation/events?expectedGeneration=GENERATION&after=CURSOR`
4
24
  uses the existing Fleet route boundary, including `/fleet/api/v1/...` through the
5
25
  prefix gateway. Use authenticated `fetch`, then read the response body. Cookies
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "1.2.0-nightly.77",
3
+ "version": "1.2.0-nightly.79",
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",