@ours.network/fleet 1.2.0-nightly.70 → 1.2.0-nightly.72

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.
Files changed (58) hide show
  1. package/dist/account-origin.d.ts +4 -1
  2. package/dist/account-origin.js +27 -2
  3. package/dist/application/conversation-history.d.ts +7 -0
  4. package/dist/application/conversation-history.js +65 -0
  5. package/dist/application/fleet-query-service.d.ts +1 -1
  6. package/dist/application/fleet-query-service.js +2 -2
  7. package/dist/application/resource-observer.d.ts +40 -0
  8. package/dist/application/resource-observer.js +278 -0
  9. package/dist/application/role-removal-service.js +1 -1
  10. package/dist/application/role-repository.d.ts +1 -1
  11. package/dist/application/role-repository.js +11 -5
  12. package/dist/application/task-layouts.js +2 -0
  13. package/dist/application/task-room-service.d.ts +2 -0
  14. package/dist/application/task-room-service.js +2 -2
  15. package/dist/build-info.json +4 -4
  16. package/dist/daemon-recovery.d.ts +13 -1
  17. package/dist/daemon-recovery.js +117 -3
  18. package/dist/monitor.d.ts +1 -0
  19. package/dist/monitor.js +1 -1
  20. package/dist/notifications/fleet-producer.d.ts +4 -1
  21. package/dist/notifications/fleet-producer.js +39 -10
  22. package/dist/notifications/outbox.d.ts +14 -1
  23. package/dist/notifications/outbox.js +51 -10
  24. package/dist/notifications/target-cleanup.d.ts +10 -4
  25. package/dist/notifications/target-cleanup.js +35 -23
  26. package/dist/notifications/task-producer.d.ts +3 -1
  27. package/dist/notifications/task-producer.js +38 -5
  28. package/dist/ops.js +1 -1
  29. package/dist/paths.d.ts +3 -0
  30. package/dist/paths.js +3 -0
  31. package/dist/rooms-tasks/add-member.js +2 -1
  32. package/dist/rooms-tasks/layout.js +2 -1
  33. package/dist/rooms-tasks/task-state.d.ts +1 -0
  34. package/dist/rooms-tasks/task-state.js +27 -11
  35. package/dist/rooms-tasks/types.d.ts +4 -1
  36. package/dist/runner.js +22 -7
  37. package/dist/subscriptions/login.d.ts +4 -3
  38. package/dist/subscriptions/login.js +14 -6
  39. package/dist/subscriptions/service.d.ts +1 -1
  40. package/dist/subscriptions/service.js +4 -4
  41. package/dist/subscriptions/store.js +3 -0
  42. package/dist/tunnel-setup-input.js +7 -1
  43. package/dist/watchdog/query.d.ts +3 -1
  44. package/dist/watchdog/query.js +2 -1
  45. package/dist/web/auth.d.ts +2 -2
  46. package/dist/web/auth.js +4 -4
  47. package/dist/web/events.d.ts +2 -0
  48. package/dist/web/events.js +9 -0
  49. package/dist/web/prefix-gateway.js +20 -10
  50. package/dist/web/resource-views.d.ts +124 -0
  51. package/dist/web/resource-views.js +424 -0
  52. package/dist/web/runtime.js +6 -3
  53. package/dist/web/server.d.ts +3 -0
  54. package/dist/web/server.js +171 -20
  55. package/dist/workspace-enrollment.js +53 -28
  56. package/dist/workspace-replacement.js +28 -10
  57. package/docs/conversation-stream.md +6 -0
  58. package/package.json +1 -1
@@ -1,2 +1,5 @@
1
- export declare const accountOrigins: readonly ["https://app.ours.network", "https://app.ours-tunnel.com"];
1
+ /** Syntax validation for an authority already selected by trusted private state/configuration. */
2
2
  export declare function validateAccountOrigin(value: unknown): string;
3
+ /** Incoming grants/payloads must match explicit operator configuration before any network action. */
4
+ export declare function validateConfiguredAccountOrigin(value: unknown): string;
5
+ export declare function validateConfiguredWorkspaceHost(value: unknown): boolean;
@@ -1,6 +1,31 @@
1
- export const accountOrigins = ['https://app.ours.network', 'https://app.ours-tunnel.com'];
1
+ /** Syntax validation for an authority already selected by trusted private state/configuration. */
2
2
  export function validateAccountOrigin(value) {
3
- if (typeof value !== 'string' || !accountOrigins.some(origin => origin === value))
3
+ let url;
4
+ try {
5
+ if (typeof value !== 'string')
6
+ throw Error();
7
+ url = new URL(value);
8
+ }
9
+ catch {
10
+ throw Error('Unsupported account origin');
11
+ }
12
+ if (url.protocol !== 'https:' || url.origin !== value || url.username || url.password)
4
13
  throw Error('Unsupported account origin');
5
14
  return value;
6
15
  }
16
+ /** Incoming grants/payloads must match explicit operator configuration before any network action. */
17
+ export function validateConfiguredAccountOrigin(value) {
18
+ const configured = validateAccountOrigin(process.env.OURS_ACCOUNT_ORIGIN);
19
+ if (value !== configured)
20
+ throw Error('Unsupported account origin');
21
+ return configured;
22
+ }
23
+ const dnsName = (value) => typeof value === 'string' && value.length <= 253 && value.includes('.') && value.split('.').every(label => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(label));
24
+ export function validateConfiguredWorkspaceHost(value) {
25
+ if (!dnsName(value))
26
+ return false;
27
+ if (process.env.OURS_WORKSPACE_HOST !== undefined)
28
+ return dnsName(process.env.OURS_WORKSPACE_HOST) && value === process.env.OURS_WORKSPACE_HOST;
29
+ const domain = process.env.OURS_WORKSPACE_DOMAIN;
30
+ return dnsName(domain) && value.endsWith('.' + domain) && !value.slice(0, -domain.length - 1).includes('.');
31
+ }
@@ -25,3 +25,10 @@ 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
+ export declare function conversationPreview(stateDir: string, signal: AbortSignal): Promise<{
32
+ text: string;
33
+ at: string;
34
+ }>;
@@ -213,3 +213,68 @@ export async function conversationHasAttachment(stateDir, attachment) {
213
213
  }
214
214
  return found;
215
215
  }
216
+ /** Passive, bounded last assistant text for directory previews. Never opens the
217
+ * ledger writer/recovery path or acquires a session/controller. An incomplete
218
+ * newest append is not committed yet; completed malformed records still fail. */
219
+ export async function conversationPreview(stateDir, signal) {
220
+ signal.throwIfAborted();
221
+ const dir = join(stateDir, '.conversation');
222
+ let files;
223
+ try {
224
+ files = (await readdir(dir)).filter(name => /^events-\d{6}\.jsonl$/.test(name)).sort().reverse();
225
+ }
226
+ catch (error) {
227
+ if (error.code === 'ENOENT')
228
+ return { text: 'No recent agent output', at: '' };
229
+ throw error;
230
+ }
231
+ const events = [];
232
+ for (const name of files) {
233
+ signal.throwIfAborted();
234
+ const file = await open(join(dir, name), 'r');
235
+ try {
236
+ const size = (await file.stat()).size, length = Math.min(size, 2 * 1024 * 1024), buffer = Buffer.alloc(length);
237
+ await file.read(buffer, 0, length, size - length);
238
+ const completeEnd = buffer.lastIndexOf(10) + 1;
239
+ // Only the newest segment can have an in-flight append. If its tail
240
+ // fills the bounded window, we cannot establish the previous preview.
241
+ if (completeEnd < length && (name !== files[0] || (size > length && completeEnd === 0)))
242
+ throw Error('preview ledger has partial record');
243
+ const lines = buffer.toString('utf8', 0, completeEnd).split('\n');
244
+ lines.pop();
245
+ if (size > length)
246
+ lines.shift();
247
+ for (let i = lines.length - 1; i >= 0 && events.length < 200; i--) {
248
+ const event = JSON.parse(lines[i]);
249
+ if (event.schemaVersion !== 1 || !Number.isSafeInteger(event.seq))
250
+ throw Error('preview ledger invalid');
251
+ events.push(event);
252
+ }
253
+ }
254
+ finally {
255
+ await file.close();
256
+ }
257
+ if (events.length >= 200)
258
+ break;
259
+ }
260
+ signal.throwIfAborted();
261
+ const assistant = (event) => ['message.chunk', 'message.replace'].includes(event.kind) && event.payload?.role === 'assistant' && typeof event.payload?.content?.type === 'string';
262
+ const end = events.findIndex(assistant);
263
+ if (end < 0)
264
+ return { text: 'No recent agent output', at: '' };
265
+ const last = events[end], parts = [];
266
+ for (let i = end; i < events.length; i++) {
267
+ const event = events[i];
268
+ if (last.messageId !== undefined) {
269
+ if (!assistant(event) || event.messageId !== last.messageId)
270
+ continue;
271
+ }
272
+ else if (!assistant(event) || event.messageId !== undefined)
273
+ break;
274
+ const content = event.payload.content;
275
+ parts.unshift(content.type === 'text' ? content.text : `[${content.type}]`);
276
+ if (event.kind === 'message.replace')
277
+ break;
278
+ }
279
+ return { text: parts.join('').replace(/\s+/g, ' ').trim().slice(0, 300) || 'No recent agent output', at: last.at ?? '' };
280
+ }
@@ -24,7 +24,7 @@ export declare class FleetQueryService {
24
24
  private readonly options;
25
25
  private readonly control;
26
26
  constructor(options: FleetQueryOptions);
27
- list(includeTemporary?: boolean): Promise<Array<{
27
+ list(includeTemporary?: boolean, strict?: boolean): Promise<Array<{
28
28
  role: RoleRecord;
29
29
  status: RoleStatus;
30
30
  capabilities: RoleCapabilities;
@@ -75,8 +75,8 @@ export class FleetQueryService {
75
75
  this.options = options;
76
76
  this.control = options.control ?? controlRequest;
77
77
  }
78
- async list(includeTemporary = false) {
79
- const roles = (await this.options.repository.list())
78
+ async list(includeTemporary = false, strict = false) {
79
+ const roles = (await this.options.repository.list(strict))
80
80
  .filter(role => (role.configured && role.lifetime === 'permanent') || (includeTemporary && role.lifetime === 'temporary'));
81
81
  return Promise.all(roles.map(async (role) => {
82
82
  const status = await this.status(role);
@@ -0,0 +1,40 @@
1
+ import { type FSWatcher } from 'node:fs';
2
+ export interface FleetResourceState {
3
+ protocolVersion: 1;
4
+ producerGeneration: string;
5
+ position: number;
6
+ health: 'healthy' | 'failed' | 'unsupported';
7
+ }
8
+ export interface FleetResourceScope {
9
+ configPath?: string;
10
+ subscribeMutations?: (listener: () => void) => () => void;
11
+ /** Durable notification owners may reinstall degraded watches without idle content polling. */
12
+ retryWatchFailures?: boolean;
13
+ roots?: string[];
14
+ /** Overrides full Fleet discovery for narrow owners such as task delivery. */
15
+ paths?: string[];
16
+ watch?: (path: string, listener: (event: string, filename: string | null) => void) => Pick<FSWatcher, 'on' | 'close'>;
17
+ }
18
+ /** Only call after durable persistence succeeds. Observers cannot fail a commit. */
19
+ export declare function publishFleetResourceCommit(path: string): void;
20
+ /** Native notifications cover supported local mounts. No idle content scan or
21
+ * silent claim of coverage after watch failure. Parents survive atomic rename.
22
+ * Discovery reads directory metadata only, never opens a controller or transcript. */
23
+ export declare function observeFleetResources(scope: FleetResourceScope, onCommitted: (state: FleetResourceState) => void, signal: AbortSignal): {
24
+ snapshot: () => {
25
+ health: "healthy" | "failed" | "unsupported";
26
+ protocolVersion: 1;
27
+ producerGeneration: string;
28
+ position: number;
29
+ };
30
+ cursor: (accepts: (path: string) => boolean, lifetime: AbortSignal) => {
31
+ snapshot: () => {
32
+ position: number;
33
+ health: "healthy" | "failed" | "unsupported";
34
+ protocolVersion: 1;
35
+ producerGeneration: string;
36
+ };
37
+ dispose: () => void;
38
+ };
39
+ dispose: () => void;
40
+ };
@@ -0,0 +1,278 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { lstatSync, statSync, readdirSync, watch } from 'node:fs';
4
+ import { homedir } from 'node:os';
5
+ import { dirname, join, resolve, sep } from 'node:path';
6
+ import { splitRootFor } from '../config.js';
7
+ import { agentsRoot, tmpRoot, stateRoot, defaultConfigPath } from '../paths.js';
8
+ // Directory discovery is shared by payload and notification owners. Each owner
9
+ // has its own lifetime/coverage; the underlying OS watch is reference counted.
10
+ const sharedWatches = new Map();
11
+ function sharedWatch(path, listener) {
12
+ const key = `${path}:${identity(path, true)}`;
13
+ let entry = sharedWatches.get(key);
14
+ if (!entry) {
15
+ const clients = new Set(), listeners = new Map();
16
+ const handle = watch(path, { persistent: false }, (event, name) => { for (const fn of [...listeners.values()])
17
+ fn(event, name); });
18
+ entry = { handle, clients, listeners };
19
+ sharedWatches.set(key, entry);
20
+ handle.on('error', error => { for (const client of [...clients])
21
+ client.emit('error', error); });
22
+ handle.on('close', () => { for (const client of [...clients])
23
+ client.emit('close'); });
24
+ }
25
+ const current = entry, client = new EventEmitter();
26
+ let closed = false;
27
+ current.clients.add(client);
28
+ current.listeners.set(client, listener);
29
+ return Object.assign(client, { close() {
30
+ if (closed)
31
+ return;
32
+ closed = true;
33
+ current.clients.delete(client);
34
+ current.listeners.delete(client);
35
+ if (!current.clients.size) {
36
+ sharedWatches.delete(key);
37
+ current.handle.close();
38
+ }
39
+ } });
40
+ }
41
+ const commits = new Set();
42
+ /** Only call after durable persistence succeeds. Observers cannot fail a commit. */
43
+ export function publishFleetResourceCommit(path) {
44
+ for (const listener of [...commits]) {
45
+ try {
46
+ listener(resolve(path));
47
+ }
48
+ catch { }
49
+ }
50
+ }
51
+ function directoryGone(error) {
52
+ return ['ENOENT', 'ENOTDIR'].includes(error.code ?? '');
53
+ }
54
+ function identity(path, followLink = false) {
55
+ try {
56
+ const value = followLink ? statSync(path) : lstatSync(path);
57
+ if (value.isSymbolicLink())
58
+ throw Error('symbolic resource directory');
59
+ return value.isDirectory() ? `${value.dev}:${value.ino}:${value.birthtimeMs}` : undefined;
60
+ }
61
+ catch (error) {
62
+ if (directoryGone(error))
63
+ return;
64
+ throw error;
65
+ }
66
+ }
67
+ function within(path, root) { return path === root || path.startsWith(root + sep); }
68
+ /** Native notifications cover supported local mounts. No idle content scan or
69
+ * silent claim of coverage after watch failure. Parents survive atomic rename.
70
+ * Discovery reads directory metadata only, never opens a controller or transcript. */
71
+ export function observeFleetResources(scope, onCommitted, signal) {
72
+ const config = resolve(scope.configPath ?? defaultConfigPath());
73
+ const roots = (scope.roots ?? [agentsRoot(), tmpRoot()]).map(p => resolve(p));
74
+ const paths = (scope.paths ?? (scope.roots && !scope.configPath ? roots : [...roots, splitRootFor(config), join(stateRoot(), 'tasks'), join(stateRoot(), 'rooms'), join(stateRoot(), 'watchdogs'), join(stateRoot(), 'layouts'), join(stateRoot(), 'member-additions'), join(stateRoot(), 'subscriptions')])).map(p => resolve(p));
75
+ // Explicit roots without config select discovery only (notification producers).
76
+ const configEnabled = scope.configPath !== undefined || (!scope.paths && !scope.roots);
77
+ const adoptedHomes = configEnabled ? [join(homedir(), '.claude'), join(homedir(), '.codex')] : [];
78
+ if (configEnabled)
79
+ paths.push(dirname(config), resolve(stateRoot()), ...adoptedHomes);
80
+ // Explicit resource locations and their ancestors already belong to the
81
+ // caller's read scope (e.g. /home -> /var/home or a linked state root).
82
+ // Discovered descendants keep lstat validation and never traverse child links.
83
+ const trustedPaths = new Set(paths);
84
+ for (const path of paths) {
85
+ let parent = path;
86
+ for (let depth = 0; depth < 4 && dirname(parent) !== parent; depth++, parent = dirname(parent))
87
+ trustedPaths.add(parent);
88
+ }
89
+ const resourceIdentity = (path) => identity(path, trustedPaths.has(path));
90
+ let state = { protocolVersion: 1, producerGeneration: randomUUID(), position: 0, health: 'healthy' };
91
+ let disposed = signal.aborted, queued = false;
92
+ let recovery;
93
+ let installing = false, installFailure;
94
+ const cursors = new Set();
95
+ const watchers = new Map();
96
+ const release = () => { const old = [...watchers.values()]; watchers.clear(); for (const entry of old)
97
+ entry.handle.close(); };
98
+ const fail = (error) => {
99
+ if (installing) {
100
+ installFailure = error;
101
+ return;
102
+ }
103
+ if (disposed || state.health !== 'healthy')
104
+ return;
105
+ state = { ...state, position: state.position + 1, health: ['ENOSYS', 'ENOTSUP', 'ERR_FEATURE_UNAVAILABLE_ON_PLATFORM'].includes(error.code ?? '') ? 'unsupported' : 'failed' };
106
+ release();
107
+ onCommitted({ ...state });
108
+ recover();
109
+ };
110
+ const install = () => {
111
+ installing = true;
112
+ installFailure = undefined;
113
+ try {
114
+ const wanted = new Set();
115
+ const cover = (dir) => {
116
+ const id = resourceIdentity(dir);
117
+ if (!id)
118
+ return;
119
+ wanted.add(dir);
120
+ const existing = watchers.get(dir);
121
+ if (existing && existing.identity === id)
122
+ return;
123
+ if (existing) {
124
+ watchers.delete(dir);
125
+ existing.handle.close();
126
+ }
127
+ let handle;
128
+ try {
129
+ handle = (scope.watch ?? sharedWatch)(dir, (_event, filename) => {
130
+ if (watchers.get(dir)?.handle !== handle)
131
+ return;
132
+ // Ignore sibling noise from ancestor coverage, lock churn and delivery acknowledgements.
133
+ const changed = filename === null ? dir : resolve(dir, String(filename));
134
+ if (changed.split(sep).some(name => name === '.locks' || name.endsWith('.lock') || name.endsWith('.tmp')))
135
+ return;
136
+ if (paths.some(path => within(changed, path) || within(path, changed)) || (configEnabled && changed === config) || filename === null)
137
+ wake(filename === null ? undefined : changed);
138
+ });
139
+ }
140
+ catch (error) {
141
+ // Parent coverage owns removal/recreation. Only verified disappearance
142
+ // is benign; permissions, unsupported watches and other errors fail.
143
+ if (directoryGone(error) && !resourceIdentity(dir))
144
+ return;
145
+ throw error;
146
+ }
147
+ watchers.set(dir, { identity: id, handle });
148
+ handle.on('error', (error) => { if (watchers.get(dir)?.handle === handle)
149
+ fail(error); });
150
+ handle.on('close', () => { if (watchers.get(dir)?.handle === handle)
151
+ fail(Error('resource watch closed')); });
152
+ if (installFailure)
153
+ throw installFailure;
154
+ };
155
+ const visit = (dir, depth) => {
156
+ const id = resourceIdentity(dir);
157
+ if (!id)
158
+ return;
159
+ // Watch before enumerating children: creation before watch is covered by
160
+ // this scan; creation after watch schedules another discovery pass.
161
+ cover(dir);
162
+ if (!depth)
163
+ return;
164
+ let entries;
165
+ try {
166
+ entries = readdirSync(dir, { withFileTypes: true });
167
+ }
168
+ catch (error) {
169
+ if (directoryGone(error) && !resourceIdentity(dir))
170
+ return;
171
+ throw error;
172
+ }
173
+ for (const entry of entries) {
174
+ if (!entry.isDirectory() || entry.isSymbolicLink() || entry.name === '.locks' || entry.name.endsWith('.lock'))
175
+ continue;
176
+ visit(join(dir, entry.name), depth - 1);
177
+ }
178
+ };
179
+ for (const path of paths) {
180
+ // Enough ancestors to retain the subscription when its state root is replaced.
181
+ let parent = path;
182
+ for (let depth = 0; depth < 4 && dirname(parent) !== parent; depth++, parent = dirname(parent)) {
183
+ if (resourceIdentity(parent)) {
184
+ wanted.add(parent);
185
+ cover(parent);
186
+ }
187
+ }
188
+ visit(path, configEnabled && (path === dirname(config) || adoptedHomes.includes(path)) ? 0 : 3);
189
+ }
190
+ for (const [dir, entry] of watchers) {
191
+ if (!wanted.has(dir) || resourceIdentity(dir) !== entry.identity) {
192
+ watchers.delete(dir);
193
+ entry.handle.close();
194
+ }
195
+ }
196
+ if (installFailure)
197
+ throw installFailure;
198
+ }
199
+ finally {
200
+ installing = false;
201
+ }
202
+ };
203
+ const recover = () => {
204
+ if (disposed || state.health === 'healthy' || !scope.retryWatchFailures || recovery)
205
+ return;
206
+ recovery = setTimeout(() => {
207
+ recovery = undefined;
208
+ if (disposed)
209
+ return;
210
+ try {
211
+ install();
212
+ }
213
+ catch {
214
+ release();
215
+ recover();
216
+ return;
217
+ }
218
+ state = { ...state, position: state.position + 1, health: 'healthy' };
219
+ // Reinstallation precedes one checkpoint recovery pass; later work is
220
+ // event driven again. Degraded retries inspect directory metadata only.
221
+ onCommitted({ ...state });
222
+ }, 3000);
223
+ recovery.unref();
224
+ };
225
+ const wake = (path) => {
226
+ if (disposed || state.health !== 'healthy')
227
+ return;
228
+ state = { ...state, position: state.position + 1 };
229
+ for (const cursor of cursors)
230
+ if (path === undefined || cursor.accepts(path))
231
+ cursor.position = state.position;
232
+ if (queued)
233
+ return;
234
+ queued = true;
235
+ queueMicrotask(() => {
236
+ queued = false;
237
+ if (disposed || state.health !== 'healthy')
238
+ return;
239
+ try {
240
+ install();
241
+ }
242
+ catch (error) {
243
+ fail(error);
244
+ return;
245
+ }
246
+ onCommitted({ ...state });
247
+ });
248
+ };
249
+ const direct = (path) => { if (paths.some(root => within(path, root)) || (configEnabled && path === config))
250
+ wake(path); };
251
+ let detach;
252
+ const dispose = () => { if (disposed)
253
+ return; disposed = true; clearTimeout(recovery); recovery = undefined; detach?.(); for (const cursor of [...cursors])
254
+ cursor.dispose(); commits.delete(direct); signal.removeEventListener('abort', dispose); release(); };
255
+ if (!disposed) {
256
+ commits.add(direct);
257
+ detach = scope.subscribeMutations?.(wake);
258
+ signal.addEventListener('abort', dispose, { once: true });
259
+ try {
260
+ install();
261
+ }
262
+ catch (error) {
263
+ fail(error);
264
+ }
265
+ }
266
+ const snapshot = () => ({ ...state, ...(disposed ? { health: 'failed' } : {}) });
267
+ // View-local cursors share coverage and health, but unrelated file commits
268
+ // cannot invalidate a capture. Unknown wakes remain conservative.
269
+ const cursor = (accepts, lifetime) => {
270
+ const entry = { accepts, position: state.position, dispose() { cursors.delete(entry); lifetime.removeEventListener('abort', entry.dispose); } };
271
+ if (!disposed && !lifetime.aborted) {
272
+ cursors.add(entry);
273
+ lifetime.addEventListener('abort', entry.dispose, { once: true });
274
+ }
275
+ return { snapshot: () => ({ ...snapshot(), position: entry.position }), dispose: entry.dispose };
276
+ };
277
+ return { snapshot, cursor, dispose };
278
+ }
@@ -100,7 +100,7 @@ export class RoleRemovalService {
100
100
  renameSync(sourceState, join(recoveryPath, 'orphan-state'));
101
101
  removeReminderState(join(recoveryPath, 'orphan-state'));
102
102
  }
103
- await retireNotificationTarget(`/fleet/chats/agents/${encodeURIComponent(preview.role)}`).catch(() => { });
103
+ await retireNotificationTarget({ kind: 'agent', agentId: preview.role }).catch(() => { });
104
104
  this.options.ops.log(`removed orphaned backend/state '${preview.role}'`);
105
105
  }
106
106
  return { ...preview, removed: true, recoveryPath };
@@ -14,7 +14,7 @@ export interface RoleRepositoryOptions {
14
14
  export declare class RoleRepository {
15
15
  private readonly options;
16
16
  constructor(options?: RoleRepositoryOptions);
17
- list(): Promise<RoleRecord[]>;
17
+ list(strict?: boolean): Promise<RoleRecord[]>;
18
18
  get(id: string): Promise<RoleRecord | undefined>;
19
19
  stateDir(record: RoleRecord): string | undefined;
20
20
  private detect;
@@ -20,13 +20,15 @@ function retainedTaskId(dir, name) {
20
20
  return undefined;
21
21
  }
22
22
  }
23
- function safeDirs(root) {
23
+ function safeDirs(root, strict = false) {
24
24
  try {
25
25
  return readdirSync(root, { withFileTypes: true })
26
26
  .filter(entry => entry.isDirectory() && !entry.isSymbolicLink() && ROLE_NAME_RE.test(entry.name))
27
27
  .map(entry => entry.name);
28
28
  }
29
- catch {
29
+ catch (error) {
30
+ if (strict && error.code !== 'ENOENT')
31
+ throw error;
30
32
  return [];
31
33
  }
32
34
  }
@@ -101,7 +103,7 @@ export class RoleRepository {
101
103
  constructor(options = {}) {
102
104
  this.options = options;
103
105
  }
104
- async list() {
106
+ async list(strict = false) {
105
107
  const permanentRoot = this.options.permanentRoot ?? agentsRoot();
106
108
  const temporaryRoot = this.options.temporaryRoot ?? tmpRoot();
107
109
  let configured = [];
@@ -110,10 +112,12 @@ export class RoleRepository {
110
112
  configured = loadConfig(this.options.configPath).roles;
111
113
  }
112
114
  catch (error) {
115
+ if (strict)
116
+ throw error;
113
117
  configProblem = { code: 'config_invalid', severity: 'error', source: 'config', detail: error.message };
114
118
  }
115
- const permanent = new Set(safeDirs(permanentRoot));
116
- const temporary = new Set(safeDirs(temporaryRoot));
119
+ const permanent = new Set(safeDirs(permanentRoot, strict));
120
+ const temporary = new Set(safeDirs(temporaryRoot, strict));
117
121
  const configuredByName = new Map(configured.map(role => [role.name, role]));
118
122
  const names = [...new Set([...configuredByName.keys(), ...permanent, ...temporary])].sort();
119
123
  return boundedMap(names, this.options.concurrency ?? 8, async (name) => {
@@ -126,6 +130,8 @@ export class RoleRepository {
126
130
  if (inTemporary) {
127
131
  const read = snapshot(join(temporaryRoot, name, 'role.yaml'), name);
128
132
  tempRole = read.role;
133
+ if (strict && read.problem)
134
+ throw new Error('temporary role snapshot unavailable');
129
135
  if (read.problem) {
130
136
  problems.push(read.problem);
131
137
  stateHealth = 'corrupt';
@@ -1,3 +1,4 @@
1
+ import { publishFleetResourceCommit } from './resource-observer.js';
1
2
  /** Task ↔ Room Layout bridge.
2
3
  * A layout task owns exactly one Room Layout run, `task-<task_id>`, snapshotted
3
4
  * from the named source layout with task context added to every room contract.
@@ -134,6 +135,7 @@ export class TaskLayouts {
134
135
  writeOperation(runId, record) {
135
136
  layoutRunPath(runId); // validates the ID and creates the private layouts directory
136
137
  replaceFileAtomically(this.operationPath(runId), JSON.stringify({ ...record, updated_at: new Date().toISOString() }), 0o600);
138
+ publishFleetResourceCommit(this.operationPath(runId));
137
139
  }
138
140
  /** Sanitized read model: no Agent Template snapshots, owner fingerprints or session internals. */
139
141
  view(task) {
@@ -184,12 +184,14 @@ export declare class TaskRoomApplicationService {
184
184
  state?: TaskState | TaskState[];
185
185
  list?: string;
186
186
  includeDeleting?: boolean;
187
+ strict?: boolean;
187
188
  }): TaskRecord[];
188
189
  listTaskLists(): TaskListRecord[];
189
190
  groupedTasks(filter?: {
190
191
  state?: TaskState | TaskState[];
191
192
  list?: string;
192
193
  includeDeleting?: boolean;
194
+ strict?: boolean;
193
195
  }): Array<{
194
196
  list: TaskListRecord;
195
197
  tasks: TaskRecord[];
@@ -136,7 +136,7 @@ export class TaskRoomApplicationService {
136
136
  return { ...task, layout_rooms: (view?.rooms ?? []).map(({ key, state, room_id, identity_cid }) => ({ key, state, ...(room_id ? { room_id } : {}), ...(identity_cid ? { identity_cid } : {}) })) };
137
137
  }
138
138
  catch {
139
- return { ...task, layout_rooms: [] };
139
+ return task; /* An unreadable run cannot prove room absence. */
140
140
  }
141
141
  }
142
142
  /** Backlog only: choose, change or clear the task's Room Layout before it starts. */
@@ -302,7 +302,7 @@ export class TaskRoomApplicationService {
302
302
  }
303
303
  listTasks(filter) {
304
304
  const listId = filter?.list === undefined ? undefined : resolveTaskList(filter.list).list_id;
305
- return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting });
305
+ return readTasks({ state: filter?.state, listId, includeDeleting: filter?.includeDeleting, strict: filter?.strict });
306
306
  }
307
307
  listTaskLists() { return readTaskLists(); }
308
308
  groupedTasks(filter) {
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "1.2.0-nightly.70",
3
- "buildId": "ca16d5313844",
4
- "commit": "16bdad930ad484741ec362a5b1f608d9c5e8706a",
2
+ "version": "1.2.0-nightly.72",
3
+ "buildId": "b6b76cb3faf6",
4
+ "commit": "912fae4f35543806656b5f0488fce830330681d5",
5
5
  "dirty": true,
6
- "builtAt": "2026-10-16T20:49:37.491Z",
6
+ "builtAt": "2026-10-18T12:25:33.807Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -1,5 +1,5 @@
1
1
  import { type AttachOursClientOptions, type OursClient } from '@ours.network/sdk/client';
2
- import { type FetchLike } from './monitor.js';
2
+ import { type FetchLike, type IdentityPresence } from './monitor.js';
3
3
  export declare const DAEMON_RECOVERY_MAX_ATTEMPTS = 6;
4
4
  export declare const DAEMON_RECOVERY_INITIAL_BACKOFF_MS = 1000;
5
5
  export declare const DAEMON_RECOVERY_MAX_BACKOFF_MS = 5000;
@@ -30,12 +30,24 @@ export type DaemonGenerationObservation = {
30
30
  reason: string;
31
31
  };
32
32
  interface GenerationProbeDeps {
33
+ requestSignal?: AbortSignal;
33
34
  readText?(path: string): string;
34
35
  canonicalize?(path: string): string;
35
36
  attachClient?(options: AttachOursClientOptions): Pick<OursClient, 'version' | 'identities' | 'close'> | Promise<Pick<OursClient, 'version' | 'identities' | 'close'>>;
36
37
  }
37
38
  /** Read generation and readiness through the selected gateway. Server paths are opaque. */
38
39
  export declare function probeDaemonGeneration(fetch: FetchLike, env: NodeJS.ProcessEnv, deps?: GenerationProbeDeps): Promise<DaemonGenerationProbe>;
40
+ export interface SelectedDaemonHealthSnapshot {
41
+ generation: DaemonGenerationProbe;
42
+ identities?: unknown[];
43
+ }
44
+ export interface SelectedDaemonHealthLease {
45
+ sample(): Promise<SelectedDaemonHealthSnapshot>;
46
+ presence(name: string, snapshot: SelectedDaemonHealthSnapshot): IdentityPresence;
47
+ release(): void;
48
+ }
49
+ /** Retained runtime health, independent of browser delivery and stream readiness. */
50
+ export declare function acquireSelectedDaemonHealth(env: NodeJS.ProcessEnv, deps?: GenerationProbeDeps): SelectedDaemonHealthLease;
39
51
  export declare class DaemonGenerationObserver {
40
52
  private current?;
41
53
  private unavailable;