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

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 (44) hide show
  1. package/dist/application/conversation-history.d.ts +7 -0
  2. package/dist/application/conversation-history.js +65 -0
  3. package/dist/application/fleet-query-service.d.ts +1 -1
  4. package/dist/application/fleet-query-service.js +2 -2
  5. package/dist/application/resource-observer.d.ts +40 -0
  6. package/dist/application/resource-observer.js +278 -0
  7. package/dist/application/role-repository.d.ts +1 -1
  8. package/dist/application/role-repository.js +11 -5
  9. package/dist/application/task-layouts.js +2 -0
  10. package/dist/application/task-room-service.d.ts +2 -0
  11. package/dist/application/task-room-service.js +2 -2
  12. package/dist/build-info.json +4 -4
  13. package/dist/daemon-recovery.d.ts +13 -1
  14. package/dist/daemon-recovery.js +117 -3
  15. package/dist/monitor.d.ts +1 -0
  16. package/dist/monitor.js +1 -1
  17. package/dist/notifications/fleet-producer.d.ts +4 -1
  18. package/dist/notifications/fleet-producer.js +36 -7
  19. package/dist/notifications/target-cleanup.d.ts +1 -0
  20. package/dist/notifications/target-cleanup.js +1 -1
  21. package/dist/notifications/task-producer.d.ts +3 -1
  22. package/dist/notifications/task-producer.js +38 -5
  23. package/dist/rooms-tasks/add-member.js +2 -1
  24. package/dist/rooms-tasks/layout.js +2 -1
  25. package/dist/rooms-tasks/task-state.d.ts +1 -0
  26. package/dist/rooms-tasks/task-state.js +6 -1
  27. package/dist/runner.js +22 -7
  28. package/dist/subscriptions/login.d.ts +4 -3
  29. package/dist/subscriptions/login.js +14 -6
  30. package/dist/subscriptions/service.d.ts +1 -1
  31. package/dist/subscriptions/service.js +4 -4
  32. package/dist/subscriptions/store.js +3 -0
  33. package/dist/watchdog/query.d.ts +3 -1
  34. package/dist/watchdog/query.js +2 -1
  35. package/dist/web/events.d.ts +2 -0
  36. package/dist/web/events.js +9 -0
  37. package/dist/web/prefix-gateway.js +20 -10
  38. package/dist/web/resource-views.d.ts +124 -0
  39. package/dist/web/resource-views.js +424 -0
  40. package/dist/web/runtime.js +4 -1
  41. package/dist/web/server.d.ts +3 -0
  42. package/dist/web/server.js +170 -19
  43. package/docs/conversation-stream.md +6 -0
  44. package/package.json +1 -1
@@ -26,6 +26,7 @@ class BaseLogin {
26
26
  provider;
27
27
  owner;
28
28
  kind;
29
+ changed;
29
30
  loginId = randomBytes(16).toString('hex');
30
31
  expiresAt;
31
32
  state = 'starting';
@@ -35,10 +36,11 @@ class BaseLogin {
35
36
  done;
36
37
  finish;
37
38
  timer;
38
- constructor(provider, owner, kind, now = Date.now()) {
39
+ constructor(provider, owner, kind, now = Date.now(), changed = () => { }) {
39
40
  this.provider = provider;
40
41
  this.owner = owner;
41
42
  this.kind = kind;
43
+ this.changed = changed;
42
44
  this.expiresAt = now + LOGIN_TTL_MS;
43
45
  this.done = new Promise(resolve => { this.finish = resolve; });
44
46
  this.timer = setTimeout(() => this.end('expired', 'login timed out'), LOGIN_TTL_MS);
@@ -64,6 +66,7 @@ class BaseLogin {
64
66
  clearTimeout(this.timer);
65
67
  this.stop();
66
68
  this.finish(state === 'succeeded');
69
+ this.changed();
67
70
  }
68
71
  /** Called by the service after verifying the new home. */
69
72
  complete(ok, error) { this.end(ok ? 'succeeded' : 'failed', error); }
@@ -76,8 +79,8 @@ export class ClaudeLogin extends BaseLogin {
76
79
  continued = false;
77
80
  /** Resolves with the CLI exit status once it exits. */
78
81
  exited;
79
- constructor(owner, bins, home, profileId) {
80
- super('claude', owner, 'paste_code');
82
+ constructor(owner, bins, home, profileId, changed) {
83
+ super('claude', owner, 'paste_code', Date.now(), changed);
81
84
  const darwin = process.platform === 'darwin';
82
85
  const args = darwin
83
86
  ? ['-q', '/dev/null', bins.claude, 'auth', 'login']
@@ -109,6 +112,7 @@ export class ClaudeLogin extends BaseLogin {
109
112
  }
110
113
  this.url = url;
111
114
  this.state = 'awaiting_user';
115
+ this.changed();
112
116
  }
113
117
  }
114
118
  // Claude asks for Enter after success; the answer carries no secret.
@@ -131,6 +135,7 @@ export class ClaudeLogin extends BaseLogin {
131
135
  throw new FleetError('invalid_request', 'that does not look like a sign-in code');
132
136
  this.submitted = true;
133
137
  this.state = 'verifying';
138
+ this.changed();
134
139
  this.output = '';
135
140
  this.child?.stdin.write(`${code}\r`);
136
141
  }
@@ -151,8 +156,8 @@ export class CodexDeviceLogin extends BaseLogin {
151
156
  codexLoginId;
152
157
  /** Resolves true when the app-server reports a successful login. */
153
158
  completed;
154
- constructor(owner, bins, home, profileId) {
155
- super('codex', owner, 'device_code');
159
+ constructor(owner, bins, home, profileId, changed) {
160
+ super('codex', owner, 'device_code', Date.now(), changed);
156
161
  this.completed = (async () => {
157
162
  try {
158
163
  this.server = await CodexAppServer.start(bins.codex, profileEnv('CODEX_HOME', profileId, home));
@@ -184,9 +189,12 @@ export class CodexDeviceLogin extends BaseLogin {
184
189
  this.url = url;
185
190
  this.userCode = String(started.userCode).slice(0, 32);
186
191
  this.state = 'awaiting_user';
192
+ this.changed();
187
193
  const ok = await Promise.race([completion, this.done.then(() => false)]);
188
- if (this.state === 'awaiting_user')
194
+ if (this.state === 'awaiting_user') {
189
195
  this.state = 'verifying';
196
+ this.changed();
197
+ }
190
198
  return ok;
191
199
  }
192
200
  catch {
@@ -71,7 +71,7 @@ export declare class SubscriptionService {
71
71
  private readonly logins;
72
72
  private readonly probes;
73
73
  constructor(options: SubscriptionServiceOptions);
74
- list(): Promise<ProviderView[]>;
74
+ list(observeOnly?: boolean): Promise<ProviderView[]>;
75
75
  /** Health + account + (Codex) usage for one profile. Concurrent calls share one run. */
76
76
  check(provider: SubscriptionProvider, profileId: string): Promise<CheckResult>;
77
77
  private checkClaude;
@@ -23,7 +23,7 @@ export class SubscriptionService {
23
23
  this.bins = options.binaries ?? defaultBinaries();
24
24
  this.now = options.now ?? Date.now;
25
25
  }
26
- async list() {
26
+ async list(observeOnly = false) {
27
27
  const state = readSubscriptionState();
28
28
  const pins = await this.pins();
29
29
  return SUBSCRIPTION_PROVIDERS.map(provider => {
@@ -31,7 +31,7 @@ export class SubscriptionService {
31
31
  const profiles = p.profiles.map(profile => {
32
32
  const key = `${provider}/${profile.id}`;
33
33
  const cached = this.checks.get(key);
34
- if (!cached || this.now() - Date.parse(cached.health.checkedAt ?? '0') > STALE_CHECK_MS)
34
+ if (!observeOnly && (!cached || this.now() - Date.parse(cached.health.checkedAt ?? '0') > STALE_CHECK_MS))
35
35
  void this.check(provider, profile.id).catch(() => { });
36
36
  const observed = provider === 'claude' ? this.observedUsage(pins, profile.id) : undefined;
37
37
  const probed = provider === 'claude' ? this.probedUsage(profile.id) : undefined;
@@ -135,8 +135,8 @@ export class SubscriptionService {
135
135
  }
136
136
  const { id: profileId, home } = createProfileHome(provider);
137
137
  const login = provider === 'claude'
138
- ? new ClaudeLogin(owner, this.bins, home, profileId)
139
- : new CodexDeviceLogin(owner, this.bins, home, profileId);
138
+ ? new ClaudeLogin(owner, this.bins, home, profileId, this.options.onChange)
139
+ : new CodexDeviceLogin(owner, this.bins, home, profileId, this.options.onChange);
140
140
  const loginId = login.view().loginId;
141
141
  this.logins.set(loginId, login);
142
142
  void this.completeLogin(provider, profileId, login);
@@ -1,3 +1,4 @@
1
+ import { publishFleetResourceCommit } from '../application/resource-observer.js';
1
2
  import { existsSync, mkdirSync, readFileSync, rmSync, symlinkSync } from 'node:fs';
2
3
  import { randomBytes } from 'node:crypto';
3
4
  import { join } from 'node:path';
@@ -94,6 +95,7 @@ export async function updateSubscriptionState(fn) {
94
95
  const state = readSubscriptionState();
95
96
  const result = fn(state);
96
97
  replaceFileAtomically(stateFile(), `${JSON.stringify(state, null, 2)}\n`, 0o600);
98
+ publishFleetResourceCommit(stateFile());
97
99
  return result;
98
100
  });
99
101
  }
@@ -137,4 +139,5 @@ export function readPin(stateDir, provider) {
137
139
  }
138
140
  export function writePin(stateDir, pin) {
139
141
  replaceFileAtomically(join(stateDir, PIN_FILE), `${JSON.stringify({ [pin.provider]: pin })}\n`, 0o600);
142
+ publishFleetResourceCommit(join(stateDir, PIN_FILE));
140
143
  }
@@ -39,7 +39,9 @@ export declare function buildWatchdogFindings(cfg: {
39
39
  * polling. `now` is injectable (defaults to Date.now) so the TTL boundary is
40
40
  * unit-testable without real timers.
41
41
  */
42
- export declare function cachedWatchdogFindingsProvider(build: () => Map<string, WatchdogRoleFinding>, ttlMs: number, now?: () => number): () => Map<string, WatchdogRoleFinding>;
42
+ export declare function cachedWatchdogFindingsProvider(build: () => Map<string, WatchdogRoleFinding>, ttlMs: number, now?: () => number): (() => Map<string, WatchdogRoleFinding>) & {
43
+ invalidate: () => void;
44
+ };
43
45
  export interface WatchdogSummary {
44
46
  name: string;
45
47
  enabled: boolean;
@@ -59,12 +59,13 @@ export function buildWatchdogFindings(cfg, latestReport) {
59
59
  */
60
60
  export function cachedWatchdogFindingsProvider(build, ttlMs, now = Date.now) {
61
61
  let cached;
62
- return () => {
62
+ const read = () => {
63
63
  const at = now();
64
64
  if (!cached || at - cached.at >= ttlMs)
65
65
  cached = { at, value: build() };
66
66
  return cached.value;
67
67
  };
68
+ return Object.assign(read, { invalidate: () => { cached = undefined; } });
68
69
  }
69
70
  const DEFAULT_REPORTS_LIMIT = 50;
70
71
  const MAX_REPORTS_LIMIT = 500;
@@ -8,6 +8,8 @@ export interface FleetEvent {
8
8
  }
9
9
  export declare class FleetEventBus {
10
10
  private readonly events;
11
+ private readonly resourceListeners;
12
+ subscribeResources(listener: () => void): () => void;
11
13
  private readonly clients;
12
14
  publish(kind: string, payload: unknown, roleId?: string): FleetEvent;
13
15
  attach(socket: WebSocket, lastEventId?: string): () => void;
@@ -1,6 +1,8 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  export class FleetEventBus {
3
3
  events = [];
4
+ resourceListeners = new Set();
5
+ subscribeResources(listener) { this.resourceListeners.add(listener); return () => { this.resourceListeners.delete(listener); }; }
4
6
  clients = new Set();
5
7
  publish(kind, payload, roleId) {
6
8
  const event = { eventId: randomUUID(), at: new Date().toISOString(), roleId, kind, payload };
@@ -12,6 +14,12 @@ export class FleetEventBus {
12
14
  if (socket.readyState === socket.OPEN && socket.bufferedAmount < 1024 * 1024)
13
15
  socket.send(frame);
14
16
  }
17
+ for (const listener of this.resourceListeners) {
18
+ try {
19
+ listener();
20
+ }
21
+ catch { }
22
+ }
15
23
  return event;
16
24
  }
17
25
  attach(socket, lastEventId) {
@@ -30,5 +38,6 @@ export class FleetEventBus {
30
38
  for (const socket of this.clients)
31
39
  socket.close(1001, 'server shutdown');
32
40
  this.clients.clear();
41
+ this.resourceListeners.clear();
33
42
  }
34
43
  }
@@ -103,17 +103,24 @@ export function createPrefixGateway(options) {
103
103
  delete headers['sec-websocket-protocol'];
104
104
  return { ...route, headers, session, authHeaders, deviceSocket: socketCredential !== undefined };
105
105
  };
106
- const track = (authHeaders, session, close) => {
106
+ const track = (authHeaders, session, close, neutral = false) => {
107
107
  if (!session)
108
108
  return () => { };
109
109
  // The session authenticated for this request, with its own deadline, not whatever is current later.
110
110
  const unbind = auth.bindTransport(session, close);
111
- const timer = setInterval(() => { try {
112
- auth.authenticate({ headers: authHeaders });
113
- }
114
- catch {
115
- close();
116
- } }, 30_000);
111
+ const timer = setInterval(() => {
112
+ if (neutral) {
113
+ if (!auth.sessionValid(session))
114
+ close();
115
+ }
116
+ else
117
+ try {
118
+ auth.authenticate({ headers: authHeaders });
119
+ }
120
+ catch {
121
+ close();
122
+ }
123
+ }, neutral ? 1000 : 30_000);
117
124
  timer.unref();
118
125
  return () => { clearInterval(timer); unbind(); };
119
126
  };
@@ -123,7 +130,7 @@ export function createPrefixGateway(options) {
123
130
  const accountCors = (req) => {
124
131
  const origin = auth?.accountOrigin(req);
125
132
  return origin ? { 'access-control-allow-origin': origin, vary: 'Origin',
126
- 'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id' } : {};
133
+ 'access-control-expose-headers': 'Accept-Ranges, Content-Disposition, Content-Length, Content-Range, Content-Type, ETag, X-Ours-Workspace-Auth, X-Ours-Request-Id, X-Resource-Producer, X-Resource-Revision, X-Resource-Source-Generation, X-Resource-Source-Position, X-Resource-Connection, X-Resource-View, X-Resource-Epoch, X-Resource-Baseline' } : {};
127
134
  };
128
135
  const server = createServer((req, res) => {
129
136
  const cors = accountCors(req);
@@ -134,7 +141,7 @@ export function createPrefixGateway(options) {
134
141
  auth.validateBoundary({ headers: req.headers, method: req.method, url: req.url }, false);
135
142
  res.writeHead(204, { ...cors, 'cache-control': 'no-store', 'access-control-max-age': '600',
136
143
  'access-control-allow-methods': 'GET, HEAD, POST, PUT, PATCH, DELETE, OPTIONS',
137
- 'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration' });
144
+ 'access-control-allow-headers': 'Authorization, Content-Type, Idempotency-Key, If-None-Match, Range, X-CSRF-Token, X-Ours-Messenger-CSRF, X-Voice-Duration, X-Resource-Connection, X-Resource-View, X-Resource-Epoch, X-Resource-Baseline' });
138
145
  res.end();
139
146
  return;
140
147
  }
@@ -186,7 +193,10 @@ export function createPrefixGateway(options) {
186
193
  res.writeHead(502, { ...(route.service ? cors : {}), 'content-type': 'application/json' });
187
194
  res.end(JSON.stringify({ error: { code: 'upstream_unavailable', message: 'Service is unavailable' } }));
188
195
  });
189
- const untrack = track(route.authHeaders, route.session, () => { upstream.destroy(); res.destroy(); });
196
+ const neutral = req.method === 'GET' && route.service?.prefix === '/notifications'
197
+ && new URL(req.url ?? '/', 'http://gateway.invalid').searchParams.get('resources') === '1'
198
+ && new URL(req.url ?? '/', 'http://gateway.invalid').pathname === '/notifications/api/v1/events';
199
+ const untrack = track(route.authHeaders, route.session, () => { upstream.destroy(); res.destroy(); }, neutral);
190
200
  res.on('close', untrack);
191
201
  req.on('aborted', () => upstream.destroy());
192
202
  res.on('close', () => upstream.destroy());
@@ -0,0 +1,124 @@
1
+ import type { ServerResponse } from 'node:http';
2
+ import { type FleetResourceScope, type FleetResourceState } from '../application/resource-observer.js';
3
+ export declare function abortableProjection<T>(read: Promise<T>, signal: AbortSignal): Promise<T>;
4
+ export interface ResourceFrameBinding {
5
+ readonly protocolVersion: 1;
6
+ readonly producerGeneration: string;
7
+ readonly subscriptionEpoch: number;
8
+ readonly viewId: string;
9
+ readonly sequence: number;
10
+ readonly revision: number;
11
+ readonly baselineId?: string;
12
+ }
13
+ export type ResourceHealthReason = 'auth_required' | 'unsupported' | 'upstream_unavailable';
14
+ export type ResourceEnvelope = ResourceFrameBinding & ({
15
+ readonly kind: 'snapshot';
16
+ readonly payload: unknown;
17
+ } | {
18
+ readonly kind: 'upsert';
19
+ readonly payload: unknown;
20
+ } | {
21
+ readonly kind: 'delete';
22
+ readonly payload: {
23
+ readonly id: string;
24
+ readonly revision: number;
25
+ };
26
+ } | {
27
+ readonly kind: 'ready';
28
+ readonly payload: {
29
+ readonly baselineId: string;
30
+ readonly source: FleetResourceState;
31
+ };
32
+ } | {
33
+ readonly kind: 'coverage';
34
+ readonly payload: {
35
+ readonly source: FleetResourceState;
36
+ };
37
+ } | {
38
+ readonly kind: 'resync';
39
+ readonly payload: {
40
+ readonly reason: 'capacity' | 'projection_unavailable';
41
+ };
42
+ } | {
43
+ readonly kind: 'health';
44
+ readonly payload: {
45
+ readonly healthy: false;
46
+ readonly reason: ResourceHealthReason;
47
+ };
48
+ });
49
+ export interface ResourceWireFrame {
50
+ event: 'resource' | 'resource-chunk';
51
+ data: Record<string, unknown>;
52
+ }
53
+ export declare const RESOURCE_FRAME_BYTES: number;
54
+ export declare const RESOURCE_PENDING_BYTES: number;
55
+ export declare const RESOURCE_ASSEMBLY_BYTES: number;
56
+ /** Lazily yield byte chunks so backpressure never queues an entire large record.
57
+ * Base64 prevents splitting a Unicode scalar from corrupting JSON/SSE parsing. */
58
+ export declare function resourceFrames(envelope: ResourceEnvelope): Generator<ResourceWireFrame>;
59
+ /** One Fleet HTTP writer. The active record owns the assembly budget;
60
+ * waiting live records share the smaller pending-output budget. */
61
+ export declare class ResourceOutput {
62
+ #private;
63
+ readonly response: ServerResponse;
64
+ readonly signal: AbortSignal;
65
+ readonly failed: (error: Error) => void;
66
+ constructor(response: ServerResponse, signal: AbortSignal, failed: (error: Error) => void);
67
+ send(envelope: ResourceEnvelope): Promise<void>;
68
+ close(error?: Error): void;
69
+ }
70
+ export interface ResourceDescriptor {
71
+ viewId: string;
72
+ resource: 'directory' | 'roles' | 'tasks' | 'task-lists' | 'configuration' | 'previews' | 'task-layout' | 'creation-action' | 'action' | 'member-addition' | 'login' | 'onboarding-health' | 'subscriptions';
73
+ entityId?: string;
74
+ receiptId?: string;
75
+ provider?: 'claude' | 'codex';
76
+ includeTemporary?: boolean;
77
+ includeDefinitions?: boolean;
78
+ ids?: string[];
79
+ state?: string;
80
+ list?: string;
81
+ groupByList?: boolean;
82
+ includeDeleting?: boolean;
83
+ }
84
+ export declare function canonicalResourceDescriptor(input: unknown): ResourceDescriptor;
85
+ interface BoundProjection {
86
+ payload: unknown;
87
+ source: FleetResourceState;
88
+ producerGeneration: string;
89
+ revision: number;
90
+ baselineId: string;
91
+ descriptor: ResourceDescriptor;
92
+ }
93
+ export declare class ResourceBindingError extends Error {
94
+ readonly status: number;
95
+ constructor(status: number, message: string);
96
+ }
97
+ /** Same committed snapshot/ready/bound-read contract as Messenger. No controllers,
98
+ * notification reads or presence are acquired by this delivery owner. */
99
+ export declare class FleetResourceViews {
100
+ private scope;
101
+ private project;
102
+ readonly producerGeneration: `${string}-${string}-${string}-${string}-${string}`;
103
+ private connections;
104
+ constructor(scope: FleetResourceScope, project: (descriptor: ResourceDescriptor, signal: AbortSignal, owner: string) => Promise<unknown>);
105
+ open(response: ServerResponse, owner: string, authorized: () => boolean): {
106
+ connectionId: `${string}-${string}-${string}-${string}-${string}`;
107
+ close: () => void;
108
+ };
109
+ register(owner: string, connectionId: string, viewId: string, epoch: number, input: unknown): {
110
+ subscriptionEpoch: number;
111
+ baselineId: string;
112
+ };
113
+ remove(owner: string, connectionId: string, viewId: string, epoch: number): void;
114
+ bound(owner: string, connectionId: string, viewId: string, epoch: number, baselineId: string): Promise<BoundProjection>;
115
+ close(): void;
116
+ private get;
117
+ private current;
118
+ private retire;
119
+ private emit;
120
+ private affects;
121
+ private schedule;
122
+ private refresh;
123
+ }
124
+ export {};