@ours.network/fleet 2.2.3 → 2.2.4

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/README.md CHANGED
@@ -1863,6 +1863,8 @@ See [Room layouts](docs/room-layouts.md) for YAML examples and CLI commands.
1863
1863
 
1864
1864
  ### Dedicated notification service
1865
1865
 
1866
+ Task room notification registration is attempted before admitting the Owner. If registration fails, room admission and task work continue; unassociated room messages may arrive as ordinary notifications even when mission-room messages are off. Existing-room registration is attempted once per Fleet startup, without automatic registration retries. The installer configures the trusted Fleet producer's mission ownership permission automatically.
1867
+
1866
1868
  Configure the web backend with `OURS_NOTIFICATIONS_ORIGIN` and a Fleet-scoped `OURS_NOTIFICATIONS_PRODUCER_TOKEN`. It reads each local supervisor ledger incrementally with persistent checkpoints under web state, correlating owner browser/channel prompts with terminal completion. Monitor/startup turns do not notify. First installation skips existing history; no agent restart or conversation controller is needed. Persist web state across deployment/restarts. Reads are bounded to 256 KiB per role per tick, and oversized model/media records are skipped across chunks. Expose `/notifications` through the generic authenticated prefix gateway with a server-side user token and `stripBrowserContext: true`; producer tokens stay server-side. All browser sessions of this single-owner console map to one stable notification account. See [ours-notifications](https://github.com/adapt-toolkit/ours-notifications) for configuration/API/release details.
1867
1869
 
1868
1870
  New agent notifications and confirmed role retirement use `/fleet/chats/agents/:roleId`, with each ID escaped once and matching message anchors retained. Deploy the app, notification service, and producers together: old query-based conversation links are intentionally unsupported. Existing pending payload URLs are replayed unchanged for idempotency; obsolete cleanup targets remain stored without being sent or treated as proof of absence. Task management notification paths are unchanged.
@@ -328,8 +328,8 @@ export declare class TaskRoomApplicationService {
328
328
  taskId: string;
329
329
  }): Promise<TaskProvisioningContinuationResult>;
330
330
  /**
331
- * Before the Owner can join, register the room as this task's with the notification service, so own-room message
332
- * preferences apply to its first message. An unreachable service stops here and the saga retries from this step.
331
+ * Attempt registration before the Owner joins. A notification failure must not become a room saga error:
332
+ * admission continues and unassociated room messages may arrive as ordinary notifications.
333
333
  */
334
334
  private associateRoomNotifications;
335
335
  private setOwnerRoomCommands;
@@ -707,8 +707,8 @@ export class TaskRoomApplicationService {
707
707
  }
708
708
  }
709
709
  /**
710
- * Before the Owner can join, register the room as this task's with the notification service, so own-room message
711
- * preferences apply to its first message. An unreachable service stops here and the saga retries from this step.
710
+ * Attempt registration before the Owner joins. A notification failure must not become a room saga error:
711
+ * admission continues and unassociated room messages may arrive as ordinary notifications.
712
712
  */
713
713
  async associateRoomNotifications(room) {
714
714
  if (!room.task_id || !room.room_identity_cid)
@@ -716,9 +716,8 @@ export class TaskRoomApplicationService {
716
716
  try {
717
717
  await (this.deps.associateRoomNotifications ?? associateRoomNotifications)(room.task_id, room.room_identity_cid);
718
718
  }
719
- catch (error) {
720
- setSagaError(room.room_id, error instanceof Error ? error.message : String(error), `Start or repair the notification service, then run ours-fleet task start ${room.task_id}.`, 'waiting_cowork');
721
- throw error;
719
+ catch {
720
+ console.warn('Mission room notification registration failed; continuing room admission with ordinary notifications');
722
721
  }
723
722
  }
724
723
  async setOwnerRoomCommands(cowork, roomId, role) {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.2.3",
3
- "buildId": "3ac91b025945",
4
- "commit": "576470bf1cd528fb2d517d081358b3e5c667eea3",
2
+ "version": "2.2.4",
3
+ "buildId": "009d1f631eac",
4
+ "commit": "ed07f55a29a98edfbcadccb5ebc07fccaa24adae",
5
5
  "dirty": false,
6
- "builtAt": "2026-10-29T08:13:26.500Z",
6
+ "builtAt": "2026-10-29T09:19:37.374Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -10,10 +10,9 @@ export declare function missionRequest(config: ProducerConfig, path: string, bod
10
10
  data: any;
11
11
  }>;
12
12
  /**
13
- * Register a room as this task's before the Owner can join it: with own-room messages off, the service must know
14
- * the room before the room can message the Owner. Unreachable service or unconfirmed coverage → throws, so room
15
- * activation waits and retries. No producer configured → nothing to gate. An older service without the route →
16
- * admitted with a warning (it cannot suppress own-room messages either).
13
+ * Attempt to register the task room before the Owner joins, so preferences apply from its first message when confirmed.
14
+ * Unreachable service or unconfirmed coverage throws to the caller, which continues admission with ordinary notifications.
15
+ * No producer configured → nothing to register. An older service without the route returns with a warning.
17
16
  */
18
17
  export declare function associateRoomNotifications(taskId: string, roomCid: string, options?: {
19
18
  config?: ProducerConfig | undefined;
@@ -47,10 +47,9 @@ async function registerMission(config, taskId, manifest, request) {
47
47
  throw new MissionOwnershipUnavailable(`notification service refused room registration (HTTP ${status})`);
48
48
  }
49
49
  /**
50
- * Register a room as this task's before the Owner can join it: with own-room messages off, the service must know
51
- * the room before the room can message the Owner. Unreachable service or unconfirmed coverage → throws, so room
52
- * activation waits and retries. No producer configured → nothing to gate. An older service without the route →
53
- * admitted with a warning (it cannot suppress own-room messages either).
50
+ * Attempt to register the task room before the Owner joins, so preferences apply from its first message when confirmed.
51
+ * Unreachable service or unconfirmed coverage throws to the caller, which continues admission with ordinary notifications.
52
+ * No producer configured → nothing to register. An older service without the route returns with a warning.
54
53
  */
55
54
  export async function associateRoomNotifications(taskId, roomCid, options = {}) {
56
55
  const manifest = recordNotificationRooms(taskId, [roomCid]);
@@ -66,7 +65,7 @@ export async function associateRoomNotifications(taskId, roomCid, options = {})
66
65
  return;
67
66
  }
68
67
  if (error instanceof MissionOwnershipUnavailable)
69
- throw new Error('Waiting for notification service to register the mission room');
68
+ throw new Error('Notification service did not register the mission room');
70
69
  throw error;
71
70
  }
72
71
  }
@@ -15,8 +15,7 @@ export declare class TaskNotificationProducer {
15
15
  private offset;
16
16
  private stopped;
17
17
  private readonly shutdown;
18
- private bootstrapped;
19
- private bootstrapAt;
18
+ private bootstrapAttempted;
20
19
  constructor(config: ProducerConfig, warn?: (line: string) => void, request?: typeof fetch,
21
20
  /** Existing tasks and their layout room CIDs, registered once per start before own-room OFF can be chosen. */
22
21
  missions?: {
@@ -16,8 +16,7 @@ export class TaskNotificationProducer {
16
16
  offset = 0;
17
17
  stopped = false;
18
18
  shutdown = new AbortController();
19
- bootstrapped = false;
20
- bootstrapAt = 0;
19
+ bootstrapAttempted = false;
21
20
  constructor(config, warn = () => { }, request = fetch,
22
21
  /** Existing tasks and their layout room CIDs, registered once per start before own-room OFF can be chosen. */
23
22
  missions) {
@@ -68,21 +67,14 @@ export class TaskNotificationProducer {
68
67
  // Deletion first: a retired task's notifications go before any other delivery work in this pass.
69
68
  if (convertRetiredTaskNotifications() || await drainMissionCleanup(this.config, this.warn, this.request))
70
69
  this.retry();
71
- if (this.missions && !this.bootstrapped && !this.stopped) {
72
- // A failed run waits 30s: unavailable inventory or a refused registration needs repair, not a tight loop.
73
- const wait = this.bootstrapAt - Date.now();
74
- if (wait > 0)
75
- this.retry(wait);
76
- else {
77
- try {
78
- await bootstrapMissionRooms(this.config, this.missions.tasks, this.missions.layoutRooms, this.request);
79
- this.bootstrapped = true;
80
- }
81
- catch {
82
- this.warn('Mission room registration pending; own-room message preference stays unavailable');
83
- this.bootstrapAt = Date.now() + 30_000;
84
- this.retry(30_000);
85
- }
70
+ if (this.missions && !this.bootstrapAttempted && !this.stopped) {
71
+ // One startup attempt. Association failure neither blocks task events nor schedules another registration.
72
+ this.bootstrapAttempted = true;
73
+ try {
74
+ await bootstrapMissionRooms(this.config, this.missions.tasks, this.missions.layoutRooms, this.request);
75
+ }
76
+ catch {
77
+ this.warn('Mission room registration failed; own-room message preference may be unavailable');
86
78
  }
87
79
  }
88
80
  const pending = pendingTaskNotifications(this.warn);
@@ -30,7 +30,7 @@ export declare class RoomLayoutService {
30
30
  supervisor(id?: string, templates?: Record<string, AgentTemplateDefinition>): NativeLayoutSupervisor;
31
31
  private owner;
32
32
  private engine;
33
- /** A task's layout rooms are registered with the notification service before the Owner can join them. */
33
+ /** A task's layout rooms attempt notification registration before Owner admission. */
34
34
  private beforeOwner;
35
35
  open(id: string, cleanup?: boolean): RoomLayout;
36
36
  /** `scope` lets an owner (e.g. a Task) snapshot an adjusted definition and pin agent working directories. */
@@ -180,7 +180,7 @@ export class RoomLayoutService {
180
180
  assertLayoutFile(path);
181
181
  return new RoomLayout(path, () => createCoworkAdapter(), this.supervisor(id), cleanup ? undefined : this.owner(), this.beforeOwner(id));
182
182
  }
183
- /** A task's layout rooms are registered with the notification service before the Owner can join them. */
183
+ /** A task's layout rooms attempt notification registration before Owner admission. */
184
184
  beforeOwner(id) {
185
185
  const taskId = /^task-([0-9a-z]{9}[0-9a-f]{8})$/.exec(id)?.[1];
186
186
  return taskId ? async (roomCid) => (await import('../notifications/mission-ownership.js')).associateRoomNotifications(taskId, roomCid) : undefined;
@@ -104,12 +104,12 @@ export declare class RoomLayout {
104
104
  private file;
105
105
  private supervisor;
106
106
  private owner?;
107
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
107
+ /** Best-effort notification registration before the Owner joins each room. */
108
108
  private beforeOwner?;
109
109
  private coworkAdapter?;
110
110
  private readonly coworkFactory;
111
111
  constructor(file: string, cowork: CoworkAdapter | (() => CoworkAdapter), supervisor: LayoutSupervisor, owner?: LayoutOwner | undefined,
112
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
112
+ /** Best-effort notification registration before the Owner joins each room. */
113
113
  beforeOwner?: ((roomCid: string) => Promise<void>) | undefined);
114
114
  private get cowork();
115
115
  snapshot(): RoomLayoutState;
@@ -43,7 +43,7 @@ export class RoomLayout {
43
43
  coworkAdapter;
44
44
  coworkFactory;
45
45
  constructor(file, cowork, supervisor, owner,
46
- /** Runs before the Owner may join each room; a failure stops here and the next pass retries it. */
46
+ /** Best-effort notification registration before the Owner joins each room. */
47
47
  beforeOwner) {
48
48
  this.file = file;
49
49
  this.supervisor = supervisor;
@@ -165,8 +165,14 @@ export class RoomLayout {
165
165
  activation_requirements: !spec.members.length && s.owner ? [{ role: s.owner.role, count: 1 }]
166
166
  : [...new Set(spec.members.map(roleFor))].map(role => ({ role, count: spec.members.filter(m => roleFor(m) === role).length })),
167
167
  }), native => { room.native = native; s.order.push(key); });
168
- if (s.owner && !room.owner_ready && this.beforeOwner)
169
- await this.beforeOwner(room.native.identity_cid);
168
+ if (s.owner && !room.owner_ready && this.beforeOwner) {
169
+ try {
170
+ await this.beforeOwner(room.native.identity_cid);
171
+ }
172
+ catch {
173
+ console.warn('Mission room notification registration failed; continuing room admission with ordinary notifications');
174
+ }
175
+ }
170
176
  if (s.owner && !room.owner_ready)
171
177
  await this.mutation(s, `owner:${key}`, async () => {
172
178
  await this.cowork.acceptInvite(room.native.room_id, this.owner.invite, { role: s.owner.role, expected_cid: s.owner.cid });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ours.network/fleet",
3
- "version": "2.2.3",
3
+ "version": "2.2.4",
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",