Skip to content
Closed
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
7 changes: 7 additions & 0 deletions .changeset/conversational-control.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@opencoven/cave-client': minor
---

Add the first bounded conversational-control authority to `@opencoven/cave-client` with Cave remaining the sole executor and canonical owner: canonical conversation create, idempotent send with one caller-visible operation UUID, explicit retry through a fresh operation UUID and `retryOfTurnId`, non-content operation reads, explicit stop, a typed resumable event stream driven by one translator for initial and resumed pages, operation-ID error propagation, no automatic replay after ambiguous transport completion, and `reconcile_required` helpers that instruct a canonical history reload.

The five Client v1 conversation operations (`conversations.create`, `messages.send`, `operations.read`, `operations.events`, `operations.stop`) are not yet declared by the authoritative Cave contract fixture (pinned producer commit `4adc97b1`), so the optional `CaveTransport` bindings stay unbound and every call reports `unsupported_operation` until the upstream Cave mutation contract lands; no speculative routes ship. The private CLI streaming renderers follow in a separate PR per the design's PR plan.
510 changes: 500 additions & 10 deletions api-baselines/cave.d.ts

Large diffs are not rendered by default.

12 changes: 10 additions & 2 deletions api-baselines/cave.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,28 @@
"entrypoints": {
".": {
"declarationFiles": [
"dist/client-BbxpTVKf.d.ts",
"dist/client-ootQTXcj.d.ts",
"dist/index.d.ts"
],
"runtimeExports": {
"dist/index.js": [
"CAVE_ANALYTICS_WINDOWS",
"CAVE_CLIENT_VERSION",
"CAVE_CONVERSATION_EVENT_TYPES",
"CAVE_CONVERSATION_OPERATION_STATES",
"CAVE_CONVERSATION_ORIGINATING_SCOPES",
"CAVE_CONVERSATION_RECONCILE_REASONS",
"CAVE_CONVERSATION_TERMINAL_STATES",
"CAVE_FAMILIAR_PROPERTIES",
"CAVE_PAIRING_SCOPES",
"CAVE_PAIRING_STATUSES",
"CaveClient",
"CaveClientError",
"CaveDiscoveryError",
"CavePairingSession",
"caveConversationReconcileReason",
"createCaveClient",
"createConversationEventTranslator",
"createDiscoveredCaveClient",
"createManagedCaveClient",
"digestCaveContractFixture",
Expand All @@ -41,13 +48,14 @@
"normalizeCaveError",
"parseCaveContractFixture",
"parseVerifiedCaveContractFixture",
"validateConversationEventCursor",
"verifyCaveContractFixtureDigest"
]
}
},
"./managed": {
"declarationFiles": [
"dist/client-BbxpTVKf.d.ts",
"dist/client-ootQTXcj.d.ts",
"dist/managed.d.ts"
],
"runtimeExports": {
Expand Down
71 changes: 71 additions & 0 deletions packages/cave/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@ record. Unix discovery still requires a positive inode.
one-page Client v1 canonical reads through `listFamiliars()`,
`listProjects()`, `listConversations()`, `getConversation()`, and
`listConversationMessages()`.
- Conversational control adds the first bounded mutation authority while Cave
remains the sole executor and canonical owner; see
[Conversational control](#conversational-control) below.
- Four bounded async iterators, `iterateFamiliars()`, `iterateProjects()`,
`iterateConversations()`, and `iterateConversationMessages()`, lazily compose
the list routes. There is intentionally no iterator for the single-item
Expand Down Expand Up @@ -472,6 +475,74 @@ return the complete Client v1 health envelope shown above. Consumers may keep
checking `health.status`, and can additionally gate pairing or feature use from
the normalized metadata.

## Conversational control

Conversational control is the first bounded mutation authority. Cave remains
the sole executor, idempotency authority, operation journal, replay authority,
stop authority, and canonical conversation owner; the SDK exposes only
constrained typed operations — never arbitrary HTTP paths, private Cave
routes, or raw transport escape hatches.

### Client methods

- `client.createConversation({ operationId, familiarId, projectId? }, options?)`
creates one empty canonical conversation for one familiar. The caller
supplies the operation UUID; Cave resolves roots, harnesses, runtimes,
titles, and origin internally.
- `client.sendConversationMessage(conversationId, { operationId, text })` sends
one text message and returns the acceptance envelope with the operation
record and a `replayed` flag. Text is preserved byte for byte. A retry is
the same route with `retryOfTurnId` instead of text:
`client.retryConversationTurn(conversationId, { operationId, retryOfTurnId })`
is a typed convenience that introduces no second producer route.
- `client.getConversationOperation(operationId)` returns the non-content
operation record (fixed codes, turn references, event bounds, timestamps —
never prompt, attachment, or bearer content).
- `client.streamConversationOperation(operationId, options?)` returns the
typed, resumable event stream. `options.timeoutMs` is one total stream
budget; each long poll receives only the remaining budget. A caller abort
closes the current event read and the generator: it never calls Stop and
never resubmits a send.
- `client.stopConversationOperation(operationId)` sends each explicit Stop
exactly once and never retries it after an ambiguous transport completion;
calling Stop again explicitly is safe.

Every mutation is dispatched exactly once. An ambiguous transport completion
never causes an automatic replay: inspect `error.operationId` (attached to
every post-acceptance error) and decide explicitly. An identical completed
mutation replays Cave's recorded result with `replayed: true`; a reused
operation key with a different canonical request hash is Cave's
`conflict / idempotency_key_reused`. Initial attachment and every resumed
stream pass through the same event translator, which validates the envelope
before event data, requires contiguous monotonic event IDs, suppresses exact
duplicates at or below the accepted cursor, and refuses gaps, reordering,
foreign operation IDs, and malformed terminal sequences as
`invalid_response`. On `reconcile_required`, reload `getConversation()` and
`listConversationMessages()` from the first page and replace — never append
to — the local projection; the SDK never fabricates omitted deltas.

### Upstream contract gap

The five Client v1 conversation operations (`conversations.create`,
`messages.send`, `operations.read`, `operations.events`, `operations.stop`)
are **not yet declared** by the authoritative Cave contract fixture this SDK
vendors (pinned producer commit `4adc97b1`). The SDK therefore ships the full
typed surface — request validation, result/event DTO parsing, the single
event translator for initial and resumed streams, operation-ID error
propagation, no-auto-replay semantics, and reconciliation helpers — while the
optional `CaveTransport` methods `createConversation`,
`sendConversationMessage`, `getConversationOperation`,
`readConversationOperationEvents`, and `stopConversationOperation` stay
unbound: every transport binding would be a speculative route. Calls today
fail with `unsupported_operation` naming the missing capability. The route
records, generated-fixture limits, event/cursor contract, and
request-hash conformance vectors are owed by the upstream Cave producer
contract; once that lands, `pnpm sync:contracts` imports the exact fixture
commit and transport bindings can be reviewed against it. The private CLI
streaming renderers (human/JSON/NDJSON ordering) are likewise staged for a
follow-up PR per the design's PR plan, as no CLI command can execute a
mutation against a real authority before that contract exists.

## Compatibility, deadlines, and retry guidance

Cave Client v1 health accepts additive Cave API updates on major version `1`
Expand Down
2 changes: 1 addition & 1 deletion packages/cave/src/canonical-reads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -478,7 +478,7 @@ function parseProject(value: unknown, field: string): CaveProject {
};
}

function parseConversation(
export function parseConversation(
value: unknown,
field: string,
): CaveConversation {
Expand Down
Loading