Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/lib/local/local-chat.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,22 @@ describe('local query adapter surface', () => {
}
});

it('reports contract and analytics as unavailable rather than inventing them', async () => {
const { store } = await seedConversations(1);
const adapter = createLocalQueryAdapter(store);

// An empty contract would read as "this familiar is permitted nothing",
// which is a different claim from "nobody asked Cave".
await expect(adapter.familiarContract(LOCAL_FAMILIAR_ID)).resolves.toEqual({
status: 'error',
code: 'service_unavailable',
});
await expect(adapter.familiarAnalytics(LOCAL_FAMILIAR_ID)).resolves.toEqual({
status: 'error',
code: 'service_unavailable',
});
});

it('never emits loading, stale, or reconcile_required', async () => {
const { store } = await seedConversations(1);
const adapter = createLocalQueryAdapter(store);
Expand All @@ -427,6 +443,8 @@ describe('local query adapter surface', () => {
adapter.listConversations(),
adapter.getConversation('missing'),
adapter.listMessages('missing'),
adapter.familiarContract('missing'),
adapter.familiarAnalytics('missing'),
])
).map((result) => result.status);

Expand Down
22 changes: 22 additions & 0 deletions src/lib/local/local-query-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import type { CaveFamiliarAnalytics, CaveFamiliarContract } from '@opencoven/cave-client';
import type {
CaveCanonicalFamiliar,
CaveConversation,
Expand Down Expand Up @@ -32,6 +33,19 @@ const INVALID_REQUEST_RESULT = Object.freeze({
code: 'invalid_request',
} as const);

/**
* A familiar's contract and its execution analytics are Cave's to report. They
* describe what a familiar has been granted and what it has since done, and a
* device holding only local conversations has neither record. Standalone mode
* answers `service_unavailable` rather than inventing an empty contract, which
* would read as "this familiar is permitted nothing" instead of "nobody asked
* Cave".
*/
const CAVE_ONLY_RESULT = Object.freeze({
status: 'error',
code: 'service_unavailable',
} as const);

const EMPTY_PAGE = Object.freeze({
data: Object.freeze([]),
cursor: Object.freeze({ hasMore: false }),
Expand Down Expand Up @@ -196,6 +210,14 @@ export function createLocalQueryAdapter(store: ChatStore): QueryAdapter {
});
},

familiarContract() {
return guard<CaveFamiliarContract>(() => CAVE_ONLY_RESULT);
},

familiarAnalytics() {
return guard<CaveFamiliarAnalytics>(() => CAVE_ONLY_RESULT);
},

invalidate() {
// Reads go straight to the in-memory index, so there is nothing stale to
// drop. Kept to satisfy the interface the shell calls on refresh.
Expand Down
Loading