@ours.network/fleet 2.3.1 → 2.4.0-nightly.1

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.
@@ -1,9 +1,9 @@
1
1
  {
2
- "version": "2.3.1",
3
- "buildId": "2fbb7f11f70a",
4
- "commit": "f7b1dda8a2cb911367ce11617ef38759ba78c5e6",
5
- "dirty": false,
6
- "builtAt": "2026-10-30T19:03:10.573Z",
2
+ "version": "2.4.0-nightly.1",
3
+ "buildId": "232666d8f084",
4
+ "commit": "c2a3010a2fd1eef69057a1c262a9bbb1c425be9f",
5
+ "dirty": true,
6
+ "builtAt": "2026-11-03T10:02:13.389Z",
7
7
  "capabilities": [
8
8
  "service.refresh-v1",
9
9
  "managed-cli.codex-reasoning-effort-v1",
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Minimal HTTP adapter for the daemon's lease-free identity reads
3
+ * (`GET /identities/<name>/contacts-activity|history`, ours-sdk
4
+ * docs/identity-reads.md). Kept local so Fleet does not need a newer SDK client;
5
+ * the shapes are the daemon contract verbatim. The ordinary workspace client
6
+ * credential is reread on every request. No identity is ever bound.
7
+ */
8
+ export interface IdentityRef {
9
+ identity: string;
10
+ expectedCid: string;
11
+ expectedProducerGeneration?: string;
12
+ }
13
+ export interface SourcePosition {
14
+ protocolVersion: 1;
15
+ producerGeneration: string;
16
+ position: number;
17
+ }
18
+ export interface ContactRootClaim {
19
+ root_cid: string;
20
+ root_name: string;
21
+ role_id: string;
22
+ }
23
+ export interface ContactsActivity {
24
+ identity: {
25
+ name: string;
26
+ cid: string;
27
+ };
28
+ source: SourcePosition;
29
+ contacts: Array<{
30
+ name: string;
31
+ container_id: string;
32
+ }>;
33
+ roots: Record<string, ContactRootClaim>;
34
+ activity: Record<string, {
35
+ at: string;
36
+ } | {
37
+ unavailable: true;
38
+ }>;
39
+ }
40
+ export interface IdentityHistoryPage {
41
+ identity: {
42
+ name: string;
43
+ cid: string;
44
+ };
45
+ source: SourcePosition;
46
+ items: Array<Record<string, unknown> & {
47
+ seq: number;
48
+ wire_id: string;
49
+ }>;
50
+ next_cursor: number | null;
51
+ }
52
+ export declare class IdentityReadError extends Error {
53
+ readonly status: number;
54
+ readonly code: string | undefined;
55
+ constructor(status: number, code: string | undefined, message: string);
56
+ }
57
+ export interface IdentityReader {
58
+ contactsActivity(ref: IdentityRef, signal?: AbortSignal): Promise<ContactsActivity>;
59
+ history(ref: IdentityRef & {
60
+ peer_cid: string;
61
+ before_seq?: number;
62
+ limit?: number;
63
+ }, signal?: AbortSignal): Promise<IdentityHistoryPage>;
64
+ }
65
+ export declare function createIdentityReader(options: {
66
+ endpoint: string;
67
+ credentialPath: string;
68
+ fetchImpl?: typeof fetch;
69
+ }): IdentityReader;
@@ -0,0 +1,47 @@
1
+ import { readPrivateFile } from '@ours.network/sdk/connector';
2
+ export class IdentityReadError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function createIdentityReader(options) {
12
+ const fetchImpl = options.fetchImpl ?? fetch;
13
+ const base = options.endpoint.replace(/\/+$/, '');
14
+ async function get(name, kind, query, signal) {
15
+ const params = new URLSearchParams();
16
+ for (const [key, value] of Object.entries(query))
17
+ if (value !== undefined)
18
+ params.set(key, String(value));
19
+ const response = await fetchImpl(`${base}/identities/${encodeURIComponent(name)}/${kind}?${params}`, {
20
+ redirect: 'error',
21
+ signal: signal ?? AbortSignal.timeout(15_000),
22
+ headers: { 'x-ours-api-token': readPrivateFile(options.credentialPath, 4096).toString('utf8').trim() },
23
+ });
24
+ let json;
25
+ try {
26
+ json = await response.json();
27
+ }
28
+ catch {
29
+ throw new IdentityReadError(response.status, undefined, `${kind}: non-JSON response (HTTP ${response.status})`);
30
+ }
31
+ if (response.ok)
32
+ return json;
33
+ const error = json?.error;
34
+ if (error && typeof error === 'object') {
35
+ const { code, message } = error;
36
+ throw new IdentityReadError(response.status, typeof code === 'string' ? code : undefined, `${kind}: ${String(message ?? code)}`);
37
+ }
38
+ throw new IdentityReadError(response.status, undefined, `${kind}: ${String(error ?? `HTTP ${response.status}`)}`);
39
+ }
40
+ return {
41
+ contactsActivity: (ref, signal) => get(ref.identity, 'contacts-activity', { expectedCid: ref.expectedCid, expectedProducerGeneration: ref.expectedProducerGeneration }, signal),
42
+ history: (ref, signal) => get(ref.identity, 'history', {
43
+ peer_cid: ref.peer_cid, before_seq: ref.before_seq, limit: ref.limit,
44
+ expectedCid: ref.expectedCid, expectedProducerGeneration: ref.expectedProducerGeneration,
45
+ }, signal),
46
+ };
47
+ }
@@ -0,0 +1,55 @@
1
+ import { type ContactsActivity, type IdentityReader } from './identity-reads.js';
2
+ import { type ChannelSources } from './sources.js';
3
+ import type { ChannelEdge, ChannelsStore } from './store.js';
4
+ export interface ResourceFrame {
5
+ kind: string;
6
+ producerGeneration: string;
7
+ position: number;
8
+ }
9
+ export interface ResourceEventSource {
10
+ resourceEvents(identity: string, options: {
11
+ signal: AbortSignal;
12
+ expectedGeneration?: string;
13
+ after?: number;
14
+ }): AsyncIterable<ResourceFrame>;
15
+ }
16
+ export interface ChannelsIndexerDeps {
17
+ store: ChannelsStore;
18
+ reader: IdentityReader;
19
+ events: ResourceEventSource;
20
+ discover(): Promise<ChannelSources>;
21
+ onChange(): void;
22
+ log(message: string): void;
23
+ debounceMs?: number;
24
+ }
25
+ /**
26
+ * Event-driven Channels projection. One passive resource-event stream per live
27
+ * perspective source; idle coverage frames are ignored and a committed frame
28
+ * only schedules work when the source position advanced. Work is debounced,
29
+ * at most one observation runs per source, and each observation reads only
30
+ * that source's contacts and activity (one lease-free daemon read), never other sources.
31
+ */
32
+ export declare class ChannelsIndexer {
33
+ private readonly deps;
34
+ private readonly watches;
35
+ private internal;
36
+ private syncing?;
37
+ private resync;
38
+ private stopped;
39
+ private readonly debounceMs;
40
+ constructor(deps: ChannelsIndexerDeps);
41
+ /** Re-derive sources and the exclusion set; start/stop streams accordingly. */
42
+ sync(): Promise<void>;
43
+ stop(): void;
44
+ private syncOnce;
45
+ /** The daemon source of `sourceCid` changed under a caller: drop its pin and observe again. */
46
+ invalidate(sourceCid: string): void;
47
+ private retire;
48
+ private follow;
49
+ private retired;
50
+ private schedule;
51
+ private run;
52
+ private observe;
53
+ }
54
+ /** Contacts of one observation, minus internal identities, as metadata-only edges. */
55
+ export declare function edgesFrom(observation: ContactsActivity, internal: Set<string>): ChannelEdge[];
@@ -0,0 +1,214 @@
1
+ import { IdentityReadError } from './identity-reads.js';
2
+ import { norm } from './sources.js';
3
+ /**
4
+ * Event-driven Channels projection. One passive resource-event stream per live
5
+ * perspective source; idle coverage frames are ignored and a committed frame
6
+ * only schedules work when the source position advanced. Work is debounced,
7
+ * at most one observation runs per source, and each observation reads only
8
+ * that source's contacts and activity (one lease-free daemon read), never other sources.
9
+ */
10
+ export class ChannelsIndexer {
11
+ deps;
12
+ watches = new Map();
13
+ internal = new Set();
14
+ syncing;
15
+ resync = false;
16
+ stopped = false;
17
+ debounceMs;
18
+ constructor(deps) {
19
+ this.deps = deps;
20
+ this.debounceMs = deps.debounceMs ?? 250;
21
+ }
22
+ /** Re-derive sources and the exclusion set; start/stop streams accordingly. */
23
+ sync() {
24
+ if (this.syncing) {
25
+ this.resync = true;
26
+ return this.syncing;
27
+ }
28
+ this.syncing = (async () => {
29
+ try {
30
+ do {
31
+ this.resync = false;
32
+ await this.syncOnce();
33
+ } while (this.resync && !this.stopped);
34
+ }
35
+ finally {
36
+ this.syncing = undefined;
37
+ }
38
+ })();
39
+ return this.syncing;
40
+ }
41
+ stop() {
42
+ this.stopped = true;
43
+ for (const watch of this.watches.values())
44
+ this.retire(watch);
45
+ this.watches.clear();
46
+ }
47
+ async syncOnce() {
48
+ if (this.stopped)
49
+ return;
50
+ const found = await this.deps.discover();
51
+ if (this.stopped)
52
+ return;
53
+ const internalChanged = found.internal.size !== this.internal.size || [...found.internal].some(cid => !this.internal.has(cid));
54
+ this.internal = found.internal;
55
+ this.deps.store.setSources(found.sources);
56
+ // Cached edges of every source, ended ones included, lose newly internal identities.
57
+ if (internalChanged)
58
+ this.deps.store.excludeContacts(found.internal);
59
+ const live = new Map(found.sources.filter(s => !s.ended).map(s => [s.cid, s]));
60
+ for (const [cid, watch] of this.watches) {
61
+ const next = live.get(cid);
62
+ if (!next || next.name !== watch.source.name) {
63
+ this.retire(watch);
64
+ this.watches.delete(cid);
65
+ }
66
+ else
67
+ watch.source = next;
68
+ }
69
+ for (const source of live.values()) {
70
+ let watch = this.watches.get(source.cid);
71
+ if (!watch) {
72
+ watch = { source, abort: new AbortController(), inflight: false, dirty: false };
73
+ this.watches.set(source.cid, watch);
74
+ void this.follow(watch);
75
+ this.schedule(watch); // bounded bootstrap/backfill for a new source
76
+ }
77
+ else if (internalChanged)
78
+ this.schedule(watch);
79
+ }
80
+ this.deps.onChange();
81
+ }
82
+ /** The daemon source of `sourceCid` changed under a caller: drop its pin and observe again. */
83
+ invalidate(sourceCid) {
84
+ const watch = this.watches.get(sourceCid.toUpperCase());
85
+ if (!watch || watch.abort.signal.aborted)
86
+ return;
87
+ watch.generation = undefined;
88
+ watch.position = undefined;
89
+ this.schedule(watch);
90
+ }
91
+ retire(watch) {
92
+ watch.abort.abort();
93
+ if (watch.timer)
94
+ clearTimeout(watch.timer);
95
+ watch.timer = undefined;
96
+ }
97
+ async follow(watch) {
98
+ let backoff = 1_000;
99
+ while (!watch.abort.signal.aborted) {
100
+ try {
101
+ for await (const frame of this.deps.events.resourceEvents(watch.source.name, {
102
+ signal: watch.abort.signal, ...(watch.generation ? { expectedGeneration: watch.generation } : {}),
103
+ })) {
104
+ backoff = 1_000;
105
+ if (frame.kind === 'coverage')
106
+ continue;
107
+ // ready/committed at the already-observed position change nothing; resync always reads.
108
+ if (frame.kind !== 'resync' && frame.producerGeneration === watch.generation && frame.position === watch.position)
109
+ continue;
110
+ if (frame.producerGeneration !== watch.generation)
111
+ watch.position = undefined;
112
+ watch.generation = frame.producerGeneration;
113
+ this.schedule(watch);
114
+ }
115
+ }
116
+ catch (error) {
117
+ if (watch.abort.signal.aborted)
118
+ return;
119
+ this.deps.log(`channels: ${watch.source.name} event stream interrupted: ${error.message}`);
120
+ }
121
+ if (this.retired(watch))
122
+ return;
123
+ // The stream ended or failed: data may be behind until it is re-established.
124
+ watch.generation = undefined;
125
+ this.deps.store.markStale(watch.source.cid);
126
+ this.deps.onChange();
127
+ await new Promise(resolve => setTimeout(resolve, backoff));
128
+ backoff = Math.min(backoff * 2, 30_000);
129
+ }
130
+ }
131
+ retired(watch) {
132
+ return this.stopped || watch.abort.signal.aborted || this.watches.get(watch.source.cid) !== watch;
133
+ }
134
+ schedule(watch) {
135
+ watch.dirty = true;
136
+ if (watch.timer || watch.inflight || watch.abort.signal.aborted)
137
+ return;
138
+ watch.timer = setTimeout(() => { watch.timer = undefined; void this.run(watch); }, this.debounceMs);
139
+ }
140
+ async run(watch) {
141
+ if (watch.inflight || watch.abort.signal.aborted)
142
+ return;
143
+ watch.inflight = true;
144
+ watch.dirty = false;
145
+ try {
146
+ const observation = await this.observe(watch);
147
+ // Retired (stopped, removed or replaced) work never touches the store or a successor.
148
+ if (!observation || this.retired(watch))
149
+ return;
150
+ this.deps.store.replaceEdges(watch.source.cid, edgesFrom(observation, this.internal), observation.source);
151
+ watch.generation = observation.source.producerGeneration;
152
+ watch.position = observation.source.position;
153
+ this.deps.onChange();
154
+ }
155
+ catch (error) {
156
+ if (this.retired(watch))
157
+ return;
158
+ this.deps.store.markStale(watch.source.cid);
159
+ this.deps.onChange();
160
+ if (error instanceof IdentityReadError && (error.code === 'identity_not_found' || error.code === 'identity_mismatch'))
161
+ void this.sync();
162
+ else
163
+ this.deps.log(`channels: ${watch.source.name} observation failed: ${error.message}`);
164
+ }
165
+ finally {
166
+ watch.inflight = false;
167
+ if (watch.dirty && !this.retired(watch))
168
+ this.schedule(watch);
169
+ }
170
+ }
171
+ async observe(watch) {
172
+ for (let attempt = 0;; attempt++) {
173
+ try {
174
+ return await this.deps.reader.contactsActivity({
175
+ identity: watch.source.name, expectedCid: watch.source.cid,
176
+ ...(watch.generation ? { expectedProducerGeneration: watch.generation } : {}),
177
+ }, watch.abort.signal);
178
+ }
179
+ catch (error) {
180
+ if (!(error instanceof IdentityReadError) || attempt >= 2)
181
+ throw error;
182
+ // A new daemon source generation (daemon restart): drop the stale pin and read again.
183
+ if (error.code === 'source_generation_changed') {
184
+ watch.generation = undefined;
185
+ watch.position = undefined;
186
+ continue;
187
+ }
188
+ // The identity committed during the read: its own wake will follow; retry briefly.
189
+ if (error.code !== 'source_changed')
190
+ throw error;
191
+ await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
192
+ if (watch.abort.signal.aborted)
193
+ return undefined;
194
+ }
195
+ }
196
+ }
197
+ }
198
+ /** Contacts of one observation, minus internal identities, as metadata-only edges. */
199
+ export function edgesFrom(observation, internal) {
200
+ const edges = [];
201
+ for (const contact of observation.contacts) {
202
+ const cid = norm(contact.container_id);
203
+ if (internal.has(cid))
204
+ continue;
205
+ const activity = observation.activity[contact.container_id];
206
+ const root = observation.roots[contact.container_id];
207
+ edges.push({
208
+ contactCid: cid, contactName: contact.name,
209
+ lastActivity: activity && 'at' in activity ? activity.at : '',
210
+ ...(root?.root_cid ? { rootCid: norm(root.root_cid), rootName: root.root_name, roleId: root.role_id } : {}),
211
+ });
212
+ }
213
+ return edges;
214
+ }
@@ -0,0 +1,30 @@
1
+ import type { RoleRepository } from '../application/role-repository.js';
2
+ import { ChannelsService } from './service.js';
3
+ export declare const channelsPrivateDir: () => string;
4
+ /**
5
+ * Build the Fleet-side Channels service over the workspace's ordinary daemon
6
+ * client profile: identity discovery, passive resource events and lease-free
7
+ * identity reads. No identity is ever bound. Returns undefined when this host
8
+ * has no daemon profile.
9
+ */
10
+ export declare function createChannels(options: {
11
+ configPath: string;
12
+ repository: RoleRepository;
13
+ log?: (message: string) => void;
14
+ }): {
15
+ service: ChannelsService;
16
+ close(): void;
17
+ } | undefined;
18
+ /**
19
+ * The files Channels source discovery reads: the manifest (Owner CID, roles), split role
20
+ * definitions, permanent and temporary role definitions, agent journals and room records.
21
+ * Conversation ledgers and other state under the role roots are not inputs.
22
+ */
23
+ export declare function channelSourceInputs(configPath: string): {
24
+ journals: string;
25
+ rooms: string;
26
+ splitRoot: string;
27
+ permanentRoles: string;
28
+ temporaryRoles: string;
29
+ affects: (changed: string) => boolean;
30
+ };
@@ -0,0 +1,125 @@
1
+ import { join, relative, resolve, sep } from 'node:path';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { attachOursClient } from '@ours.network/sdk/client';
4
+ import { readClientProfile } from '../client-profile.js';
5
+ import { loadConfig, splitRootFor } from '../config.js';
6
+ import { agentsRoot, stateRoot, tmpRoot } from '../paths.js';
7
+ import { observeFleetResources, publishFleetResourceCommit } from '../application/resource-observer.js';
8
+ import { listRoomRecords } from '../rooms-tasks/room-state.js';
9
+ import { privateRuntimeRoot } from '../agent-ours/service.js';
10
+ import { createIdentityReader } from './identity-reads.js';
11
+ import { ChannelsIndexer } from './indexer.js';
12
+ import { ChannelsService } from './service.js';
13
+ import { readAgentRecords, resolveSources } from './sources.js';
14
+ import { ChannelsStore } from './store.js';
15
+ export const channelsPrivateDir = () => join(stateRoot(), 'private-channels');
16
+ /**
17
+ * Build the Fleet-side Channels service over the workspace's ordinary daemon
18
+ * client profile: identity discovery, passive resource events and lease-free
19
+ * identity reads. No identity is ever bound. Returns undefined when this host
20
+ * has no daemon profile.
21
+ */
22
+ export function createChannels(options) {
23
+ let profile;
24
+ try {
25
+ profile = readClientProfile();
26
+ }
27
+ catch {
28
+ return undefined;
29
+ }
30
+ const log = options.log ?? (message => console.warn(message));
31
+ const store = new ChannelsStore(channelsPrivateDir());
32
+ const reader = createIdentityReader({ endpoint: profile.endpoint, credentialPath: profile.credentialPath });
33
+ const attach = () => attachOursClient({
34
+ endpoint: profile.endpoint, expectedInstanceId: profile.expectedInstanceId, credentialPath: profile.credentialPath,
35
+ sessionMode: 'external', leaseToken: `ours-fleet-channels-${process.pid}-${randomUUID()}`, env: {},
36
+ });
37
+ let eventsClient;
38
+ const events = {
39
+ async *resourceEvents(identity, opts) {
40
+ eventsClient ??= await attach();
41
+ yield* eventsClient.resourceEvents(identity, opts);
42
+ },
43
+ };
44
+ let last;
45
+ const discover = async () => {
46
+ const ownerCid = loadConfig(options.configPath).rooms?.owner.expected_cid;
47
+ const roleByIdentity = new Map();
48
+ for (const role of await options.repository.list(true))
49
+ if (role.config?.identity)
50
+ roleByIdentity.set(role.config.identity, role.id);
51
+ const client = await attach();
52
+ let identities;
53
+ try {
54
+ identities = (await client.listIdentities());
55
+ }
56
+ finally {
57
+ await client.close().catch(() => { });
58
+ }
59
+ const found = resolveSources({
60
+ ownerCid: ownerCid ?? '',
61
+ identities,
62
+ agents: readAgentRecords(privateRuntimeRoot()),
63
+ roleByIdentity,
64
+ roomCids: listRoomRecords().map(room => room.room_identity_cid).filter((cid) => typeof cid === 'string'),
65
+ });
66
+ if (!ownerCid)
67
+ found.internal.delete('');
68
+ last = found;
69
+ return found;
70
+ };
71
+ const indexer = new ChannelsIndexer({ store, reader, events, discover, log, onChange: () => publishFleetResourceCommit(store.path) });
72
+ const sync = () => { void indexer.sync().catch(error => log(`channels: sync failed: ${error.message}`)); };
73
+ const service = new ChannelsService({ store, reader, indexer, ownerRoot: () => last?.rootCid, ready: () => last !== undefined });
74
+ sync();
75
+ // Re-derive sources when one of their inputs commits, so new or stopped agents change
76
+ // perspectives without polling. Bursts coalesce. The configuration scope also tracks the
77
+ // manifest file itself; every wake is filtered to the actual inputs (never this index).
78
+ const lifetime = new AbortController();
79
+ const inputs = channelSourceInputs(options.configPath);
80
+ let pending, relevant = false;
81
+ observeFleetResources({
82
+ configPath: options.configPath, retryWatchFailures: true,
83
+ paths: [inputs.journals, inputs.rooms, inputs.splitRoot, inputs.permanentRoles, inputs.temporaryRoles],
84
+ onInvalidated: path => { if (path === undefined || inputs.affects(path))
85
+ relevant = true; },
86
+ }, () => {
87
+ if (!relevant || pending || lifetime.signal.aborted)
88
+ return;
89
+ relevant = false;
90
+ pending = setTimeout(() => { pending = undefined; sync(); }, 1_000);
91
+ pending.unref?.();
92
+ }, lifetime.signal);
93
+ return {
94
+ service,
95
+ close() { lifetime.abort(); if (pending)
96
+ clearTimeout(pending); indexer.stop(); void eventsClient?.close().catch(() => { }); store.close(); },
97
+ };
98
+ }
99
+ /**
100
+ * The files Channels source discovery reads: the manifest (Owner CID, roles), split role
101
+ * definitions, permanent and temporary role definitions, agent journals and room records.
102
+ * Conversation ledgers and other state under the role roots are not inputs.
103
+ */
104
+ export function channelSourceInputs(configPath) {
105
+ const config = resolve(configPath);
106
+ const journals = resolve(privateRuntimeRoot()), rooms = resolve(stateRoot(), 'rooms'), splitRoot = resolve(splitRootFor(config));
107
+ const permanentRoles = resolve(agentsRoot()), temporaryRoles = resolve(tmpRoot());
108
+ const under = (path, root) => { const local = relative(root, path); return local !== '..' && !local.startsWith('..' + sep) && !local.startsWith(sep) ? local : undefined; };
109
+ const affects = (changed) => {
110
+ const path = resolve(changed);
111
+ if (path === config || under(path, journals) !== undefined || under(path, rooms) !== undefined || under(path, splitRoot) !== undefined)
112
+ return true;
113
+ for (const root of [permanentRoles, temporaryRoles]) {
114
+ const local = under(path, root);
115
+ if (local === undefined)
116
+ continue;
117
+ const parts = local.split(sep).filter(Boolean);
118
+ // The roles directory, a role directory appearing/disappearing, or its definition.
119
+ return parts.length <= 1 || (parts.length === 2 && parts[1] === 'role.yaml');
120
+ }
121
+ // Ancestor replacement of any input root.
122
+ return [config, journals, rooms, splitRoot, permanentRoles, temporaryRoles].some(root => under(root, path) !== undefined);
123
+ };
124
+ return { journals, rooms, splitRoot, permanentRoles, temporaryRoles, affects };
125
+ }
@@ -0,0 +1,61 @@
1
+ import { type IdentityHistoryPage, type IdentityReader } from './identity-reads.js';
2
+ import type { ChannelsIndexer } from './indexer.js';
3
+ import type { ChannelPage, ChannelPerspective, ChannelRow, ChannelsStore } from './store.js';
4
+ export interface ChannelsServiceDeps {
5
+ store: ChannelsStore;
6
+ reader: IdentityReader;
7
+ indexer: ChannelsIndexer;
8
+ /** Actual root CID of this workspace's Owner, when known. */
9
+ ownerRoot(): string | undefined;
10
+ /** False until the first discovery completed. */
11
+ ready(): boolean;
12
+ }
13
+ export type ChannelRowView = ChannelRow & {
14
+ claimsOwnerRoot: boolean;
15
+ };
16
+ /**
17
+ * Owner-facing Channels queries over the durable projection. Served only
18
+ * behind the Fleet web Owner session; agents get no route to it.
19
+ */
20
+ export declare class ChannelsService {
21
+ private readonly deps;
22
+ constructor(deps: ChannelsServiceDeps);
23
+ status(): {
24
+ ready: boolean;
25
+ sources: ReturnType<ChannelsStore['sourcesSummary']>;
26
+ };
27
+ private view;
28
+ list(options: {
29
+ cursor?: string;
30
+ limit?: number;
31
+ q?: string;
32
+ root?: string;
33
+ }): ChannelPage & {
34
+ channels: ChannelRowView[];
35
+ };
36
+ /** The SSE snapshot: the first Recent page. */
37
+ snapshot(): {
38
+ ready: boolean;
39
+ channels: ChannelRowView[];
40
+ nextCursor: string | null;
41
+ };
42
+ get(cid: string): {
43
+ channel: ChannelRowView;
44
+ perspectives: ChannelPerspective[];
45
+ };
46
+ /**
47
+ * One perspective's own history with this contact; never a merged view, never consuming.
48
+ * Pinned to the source's CID and last observed daemon source generation; a response is
49
+ * released only while that same perspective is still current.
50
+ */
51
+ history(contactCid: string, sourceCid: string, page: {
52
+ before_seq?: number;
53
+ limit?: number;
54
+ }): Promise<IdentityHistoryPage & {
55
+ perspective: {
56
+ sourceCid: string;
57
+ kind: string;
58
+ roleId?: string;
59
+ };
60
+ }>;
61
+ }