@ours.network/fleet 2.2.2 → 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 +2 -0
- package/dist/application/conversation-history.js +165 -14
- package/dist/application/task-room-service.d.ts +2 -2
- package/dist/application/task-room-service.js +4 -5
- package/dist/build-info.json +4 -4
- package/dist/notifications/mission-ownership.d.ts +3 -4
- package/dist/notifications/mission-ownership.js +4 -5
- package/dist/notifications/task-producer.d.ts +1 -2
- package/dist/notifications/task-producer.js +9 -17
- package/dist/rooms-tasks/layout-service.d.ts +1 -1
- package/dist/rooms-tasks/layout-service.js +1 -1
- package/dist/rooms-tasks/layout.d.ts +2 -2
- package/dist/rooms-tasks/layout.js +9 -3
- package/package.json +1 -1
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.
|
|
@@ -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,
|
|
@@ -328,8 +328,8 @@ export declare class TaskRoomApplicationService {
|
|
|
328
328
|
taskId: string;
|
|
329
329
|
}): Promise<TaskProvisioningContinuationResult>;
|
|
330
330
|
/**
|
|
331
|
-
*
|
|
332
|
-
*
|
|
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
|
-
*
|
|
711
|
-
*
|
|
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
|
|
720
|
-
|
|
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) {
|
package/dist/build-info.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.
|
|
3
|
-
"buildId": "
|
|
4
|
-
"commit": "
|
|
2
|
+
"version": "2.2.4",
|
|
3
|
+
"buildId": "009d1f631eac",
|
|
4
|
+
"commit": "ed07f55a29a98edfbcadccb5ebc07fccaa24adae",
|
|
5
5
|
"dirty": false,
|
|
6
|
-
"builtAt": "2026-10-
|
|
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
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
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('
|
|
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
|
|
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
|
-
|
|
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.
|
|
72
|
-
//
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
this.
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
+
"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",
|