@volter/twin-cohere 0.1.0

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 (40) hide show
  1. package/README.md +224 -0
  2. package/defaults/handlers.json +26 -0
  3. package/dist/defaults/handlers.json +26 -0
  4. package/dist/src/cli.d.ts +2 -0
  5. package/dist/src/cli.js +31 -0
  6. package/dist/src/cohere-budget.d.ts +55 -0
  7. package/dist/src/cohere-budget.js +171 -0
  8. package/dist/src/cohere-capabilities.d.ts +14 -0
  9. package/dist/src/cohere-capabilities.js +1852 -0
  10. package/dist/src/cohere-conformance.d.ts +17 -0
  11. package/dist/src/cohere-conformance.js +464 -0
  12. package/dist/src/cohere-connector.d.ts +150 -0
  13. package/dist/src/cohere-connector.js +625 -0
  14. package/dist/src/cohere-models.d.ts +21 -0
  15. package/dist/src/cohere-models.js +73 -0
  16. package/dist/src/cohere-scenario.d.ts +57 -0
  17. package/dist/src/cohere-scenario.js +176 -0
  18. package/dist/src/cohere-server.d.ts +16 -0
  19. package/dist/src/cohere-server.js +184 -0
  20. package/dist/src/cohere-stub.d.ts +119 -0
  21. package/dist/src/cohere-stub.js +321 -0
  22. package/dist/src/cohere-twin.d.ts +82 -0
  23. package/dist/src/cohere-twin.js +1243 -0
  24. package/dist/src/cohere-types.d.ts +226 -0
  25. package/dist/src/cohere-types.js +40 -0
  26. package/dist/src/index.d.ts +15 -0
  27. package/dist/src/index.js +84 -0
  28. package/package.json +71 -0
  29. package/src/cli.ts +30 -0
  30. package/src/cohere-budget.ts +197 -0
  31. package/src/cohere-capabilities.ts +1855 -0
  32. package/src/cohere-conformance.ts +489 -0
  33. package/src/cohere-connector.ts +709 -0
  34. package/src/cohere-models.ts +79 -0
  35. package/src/cohere-scenario.ts +194 -0
  36. package/src/cohere-server.ts +195 -0
  37. package/src/cohere-stub.ts +337 -0
  38. package/src/cohere-twin.ts +1290 -0
  39. package/src/cohere-types.ts +231 -0
  40. package/src/index.ts +159 -0
@@ -0,0 +1,709 @@
1
+ // Cohere CONNECTOR — the live-vendor pull/push path that gives the Cohere twin the full
2
+ // "git for SaaS" lifecycle (pull real state → mirror; push local writes → real).
3
+ //
4
+ // PULL (real → twin): fetch real Datasets / Connectors / Embed jobs, map the vendor JSON →
5
+ // SyncResource[], fold into the event log via syncPull (shadow-diff dedup,
6
+ // so re-pulling identical state appends nothing).
7
+ // PUSH (twin → real): for every PENDING local action (create / update / cancel / delete), call
8
+ // the real Cohere REST API and confirmAction on success (records the
9
+ // confirmed fields as an observed event + suppresses the local projection —
10
+ // counted exactly once).
11
+ //
12
+ // The vendor I/O is an INJECTED executor (B3 auth boundary): the kernel + this pack hold NO Cohere
13
+ // key and import NO SDK at runtime. Tests pass a fake executor; live runs pass
14
+ // `liveCohereExecute(apiKey)`. Same code path either way — fully exercisable offline.
15
+ import { assertBudgetGuardIntact, confirmAction, pendingActions, projectResources, syncPull } from '@volter/world-core';
16
+ import type { SyncResource, TwinAction } from '@volter/world-core';
17
+ import { CohereBudget, CohereBudgetError, cohereCallWeight, type CohereBudgetOptions } from './cohere-budget.ts';
18
+
19
+ const SERVICE = 'cohere';
20
+
21
+ /** The real Cohere API base — `CohereEnvironment.Production` in cohere-ai@8.1.0's
22
+ * `environments.d.ts`, which is the ONLY host either SDK ships. */
23
+ export const COHERE_API_BASE = 'https://api.cohere.com';
24
+
25
+ /**
26
+ * The injected real-Cohere boundary. `request` issues ONE Cohere REST call:
27
+ * method — 'GET' | 'POST' | 'PATCH' | 'DELETE'
28
+ * path — e.g. '/v1/datasets' or '/v1/embed-jobs/<id>/cancel'
29
+ * body — JSON body for POST/PATCH (omitted otherwise)
30
+ * Returns the parsed JSON: one of Cohere's per-collection list envelopes, a single object, or its
31
+ * `{ message }` error envelope.
32
+ */
33
+ export type CohereExecute = (
34
+ method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
35
+ path: string,
36
+ body?: Record<string, unknown>,
37
+ ) => Promise<unknown>;
38
+
39
+ /** Construction options for the live executor. `budget` cannot be null and cannot be loosened. */
40
+ export type LiveCohereOptions = {
41
+ /** Injected `fetch`, so a test can COUNT the requests the guard did or did not let through. */
42
+ fetchImpl?: typeof fetch;
43
+ /** An existing budget to share across executors. Omit and one is constructed. Cannot be null. */
44
+ budget?: CohereBudget;
45
+ /** Construction options for the default budget (ledger path, clock). Cannot loosen it. */
46
+ budgetOptions?: CohereBudgetOptions;
47
+ };
48
+
49
+ /**
50
+ * A live executor against the real Cohere REST API (the user's own API key). Sends
51
+ * `Authorization: Bearer <key>` — the scheme BOTH SDKs use (cohere-ai's `BearerAuthProvider`,
52
+ * which falls back to the `CO_API_KEY` env var, and @ai-sdk/cohere's provider, which reads
53
+ * `COHERE_API_KEY`). Never imported by the pack's own serving path — only constructed by a caller
54
+ * that opts into real I/O.
55
+ *
56
+ * THIS IS THE ONE PLACE this pack issues a live api.cohere.com request, and therefore the one
57
+ * place the rate budget has to be enforced. EVERY call is guarded: the budget is charged BEFORE
58
+ * the request goes out (`checkBudget`, which THROWS `CohereBudgetError` instead of returning when
59
+ * the ceiling or a cooldown says stop) and the response is fed back (`recordCall`) so a
60
+ * `retry-after` / 429 signal becomes a persisted cooldown that makes every later call fail fast
61
+ * WITHOUT touching Cohere. There is deliberately no OPTION to disable the guard, and no value a
62
+ * caller can pass for `budget` that yields an unguarded client. What that does NOT claim is
63
+ * immunity from a caller who WANTS one: a fresh `budgetOptions.path` per construction, or an
64
+ * injected clock, restores the allowance, because the same seam tests need cannot be denied to a
65
+ * determined caller in the same process. See `cohere-budget.ts` and the kernel's `rateBudget.ts`
66
+ * header for the limits of the guarantee.
67
+ */
68
+ export function liveCohereExecute(
69
+ apiKey: string,
70
+ base = COHERE_API_BASE,
71
+ opts: LiveCohereOptions = {},
72
+ ): CohereExecute {
73
+ const doFetch = opts.fetchImpl ?? fetch;
74
+ // ONE expression decides which budget is used, so there is no second, weaker test that could
75
+ // disagree with the first. `null`/`undefined` (or omitting it) build the default; anything else
76
+ // must be an UNMODIFIED CohereBudget — a duck-typed stand-in, a SUBCLASS overriding
77
+ // `checkBudget`, and a Proxy trapping it are ALL refused, because each is a one-liner that would
78
+ // otherwise hand back a client with no ceiling.
79
+ const budget = opts.budget !== undefined && opts.budget !== null
80
+ ? assertBudgetGuardIntact(opts.budget, CohereBudget, 'liveCohereExecute')
81
+ : new CohereBudget({ token: apiKey, ...(opts.budgetOptions ?? {}) });
82
+ return async (method, path, body) => {
83
+ const headers: Record<string, string> = { authorization: `Bearer ${apiKey}`, accept: 'application/json' };
84
+ const init: { method: string; headers: Record<string, string>; body?: string } = { method, headers };
85
+ if (method === 'POST' || method === 'PATCH') {
86
+ headers['content-type'] = 'application/json';
87
+ init.body = JSON.stringify(body ?? {});
88
+ }
89
+ const weight = cohereCallWeight(method, path);
90
+ // THROWS instead of calling. Nothing below this line runs when the budget refuses.
91
+ const reservation = budget.checkBudget(weight);
92
+ const res = await doFetch(`${base}${path}`, init);
93
+ const resHeaders: Record<string, string> = {};
94
+ res.headers.forEach((v: string, k: string) => { resHeaders[k.toLowerCase()] = v; });
95
+ // READ AS TEXT, then parse only if there is something to parse. `res.json()` throws a
96
+ // SyntaxError on an empty body — and the vendor HAS body-less responses (`embedJobs.cancel`
97
+ // is declared `-> void`). That throw would land BEFORE `recordCall`, leaving the budget
98
+ // reservation unsettled and dropping a `retry-after` on that very response on the floor.
99
+ // Settlement must not depend on the body parsing.
100
+ const text = await res.text();
101
+ // Settles the reservation and, on a back-off signal, arms the cooldown. May itself throw (a
102
+ // `retry-after` beyond the cap is not something to sleep off) — the cooldown is persisted
103
+ // first either way, so the refusal survives the throw.
104
+ // recordCall may THROW after arming the cooldown (a back-off beyond the cap). On a refused
105
+ // call that louder refusal wins; an answer Cohere ACCEPTED is kept, so a write that landed is
106
+ // never recorded as failed and performed again on retry.
107
+ try {
108
+ budget.recordCall(weight, resHeaders, { status: res.status, reservation });
109
+ } catch (error) {
110
+ if (!(error instanceof CohereBudgetError) || !res.ok) throw error;
111
+ }
112
+ if (text.trim() === '') return {};
113
+ try {
114
+ return JSON.parse(text) as unknown;
115
+ } catch {
116
+ throw new Error(`cohere: ${method} ${path} answered ${res.status} with a body that is not JSON: ${text.slice(0, 200)}`);
117
+ }
118
+ };
119
+ }
120
+
121
+ /**
122
+ * A REFUSED pull is NOT an empty account. Cohere answers a failure with its BARE `{ message }`
123
+ * envelope; mapping that to "no resources" would fold an empty account OVER real observed state
124
+ * and silently delete the mirror. Throw instead.
125
+ *
126
+ * Note the envelope is what identifies a refusal, not the HTTP status — the executor has already
127
+ * consumed the status, and a caller supplying its own executor may not preserve it at all.
128
+ */
129
+ function throwIfError(res: unknown, ctx: string): void {
130
+ const e = res as { message?: unknown } | null;
131
+ if (e && typeof e === 'object' && typeof e.message === 'string') {
132
+ throw new Error(`cohere ${ctx} failed: ${e.message}`);
133
+ }
134
+ }
135
+
136
+ /**
137
+ * Read one collection out of Cohere's per-collection list envelope.
138
+ *
139
+ * Cohere does NOT use a uniform `{ data: [...] }` wrapper: datasets answer `{ datasets }`,
140
+ * connectors `{ connectors, total_count }`, embed jobs `{ embed_jobs }`. Reading a shape the
141
+ * collection does not use would silently yield zero rows — which `syncPull` would then fold as an
142
+ * emptied account.
143
+ *
144
+ * The key must be PRESENT. Cohere's own schemas make each list key optional-and-nullable, so an
145
+ * explicit `null` genuinely means "no rows" and is accepted; a response with the key MISSING
146
+ * ENTIRELY is not an empty account, it is a response this connector does not understand, and it
147
+ * throws rather than reporting zero.
148
+ */
149
+ function listOf(res: unknown, key: string, nullable: boolean, ctx: string): Array<Record<string, unknown>> {
150
+ if (!res || typeof res !== 'object' || Array.isArray(res)) {
151
+ throw new Error(`cohere ${ctx} failed: expected an object carrying "${key}", got ${JSON.stringify(res).slice(0, 120)}`);
152
+ }
153
+ if (!Object.hasOwn(res as object, key)) {
154
+ throw new Error(`cohere ${ctx} failed: the response has no "${key}" key — an unrecognized envelope is not an empty account`);
155
+ }
156
+ const v = (res as Record<string, unknown>)[key];
157
+ if (v === null || v === undefined) {
158
+ // NULLABILITY IS PER COLLECTION, read from the vendor's own schemas — not assumed uniform.
159
+ // `DatasetsListResponse.datasets` and `ListEmbedJobResponse.embed_jobs` are declared
160
+ // `?: Raw[] | null`, so an explicit null genuinely means "no rows". `ListConnectorsResponse`
161
+ // declares `connectors: Connector.Raw[]` REQUIRED and NON-nullable, so a null there is a
162
+ // response the vendor says cannot occur — accepting it as an empty account would fold an empty
163
+ // list over real observed connector state, which is the exact hazard this module exists to
164
+ // close (§9 round 1, connector m1: an earlier version waved all three through on a premise
165
+ // that was false for one of them).
166
+ if (nullable) return [];
167
+ throw new Error(`cohere ${ctx} failed: "${key}" is null, but the vendor declares it required and non-nullable — an unexpected null is not an empty account`);
168
+ }
169
+ if (!Array.isArray(v)) throw new Error(`cohere ${ctx} failed: "${key}" is not a list`);
170
+ return v as Array<Record<string, unknown>>;
171
+ }
172
+
173
+ // ── PULL ────────────────────────────────────────────────────────────────────
174
+
175
+ /** Map a real-Cohere Dataset → a twin sync resource. Every field the vendor's `Dataset` schema
176
+ * declares REQUIRED (`id`, `name`, `created_at`, `updated_at`, `dataset_type`,
177
+ * `validation_status`) is carried; a pulled row that lost one would be unparseable by the
178
+ * official SDK the moment the twin served it back. */
179
+ export function mapDataset(d: Record<string, unknown>): SyncResource {
180
+ return {
181
+ type: 'dataset',
182
+ id: String(d.id),
183
+ fields: {
184
+ name: (d.name as string) ?? null,
185
+ created_at: (d.created_at as string) ?? null,
186
+ updated_at: (d.updated_at as string) ?? (d.created_at as string) ?? null,
187
+ dataset_type: (d.dataset_type as string) ?? null,
188
+ validation_status: (d.validation_status as string) ?? null,
189
+ validation_error: (d.validation_error as string) ?? null,
190
+ validation_warnings: (d.validation_warnings as string[]) ?? [],
191
+ required_fields: (d.required_fields as string[]) ?? [],
192
+ preserve_fields: (d.preserve_fields as string[]) ?? [],
193
+ dataset_parts: (d.dataset_parts as unknown[]) ?? [],
194
+ },
195
+ };
196
+ }
197
+
198
+ /** Map a real-Cohere Connector → a twin sync resource. */
199
+ export function mapConnector(c: Record<string, unknown>): SyncResource {
200
+ return {
201
+ type: 'connector',
202
+ id: String(c.id),
203
+ fields: {
204
+ organization_id: (c.organization_id as string) ?? null,
205
+ name: (c.name as string) ?? null,
206
+ description: (c.description as string) ?? null,
207
+ url: (c.url as string) ?? null,
208
+ created_at: (c.created_at as string) ?? null,
209
+ updated_at: (c.updated_at as string) ?? (c.created_at as string) ?? null,
210
+ excludes: (c.excludes as string[]) ?? [],
211
+ auth_type: (c.auth_type as string) ?? null,
212
+ oauth: (c.oauth as Record<string, unknown>) ?? null,
213
+ auth_status: (c.auth_status as string) ?? null,
214
+ active: c.active === undefined ? true : c.active === true,
215
+ continue_on_failure: c.continue_on_failure === true,
216
+ },
217
+ };
218
+ }
219
+
220
+ /**
221
+ * Map a real-Cohere EmbedJob → a twin sync resource.
222
+ *
223
+ * The vendor's key for an embed job is `job_id`, NOT `id` — there is no `id` on the schema at all.
224
+ * Subjecting the resource under `d.id` would make every pulled job land under the string
225
+ * "undefined" and collapse the whole collection onto one row.
226
+ */
227
+ export function mapEmbedJob(j: Record<string, unknown>): SyncResource {
228
+ return {
229
+ type: 'embed_job',
230
+ id: String(j.job_id),
231
+ fields: {
232
+ job_id: String(j.job_id),
233
+ name: (j.name as string) ?? null,
234
+ status: (j.status as string) ?? null,
235
+ created_at: (j.created_at as string) ?? null,
236
+ input_dataset_id: (j.input_dataset_id as string) ?? null,
237
+ output_dataset_id: (j.output_dataset_id as string) ?? null,
238
+ model: (j.model as string) ?? null,
239
+ truncate: (j.truncate as string) ?? null,
240
+ },
241
+ };
242
+ }
243
+
244
+ /** `nullable` mirrors each list key's own declaration in cohere-ai@8.1.0's serializers:
245
+ * `DatasetsListResponse.datasets?: Raw[] | null`, `ListEmbedJobResponse.embed_jobs?: Raw[] | null`,
246
+ * but `ListConnectorsResponse.connectors: Connector.Raw[]` — required and non-nullable. */
247
+ const COLLECTIONS: Array<{ path: string; key: string; nullable: boolean; map: (r: Record<string, unknown>) => SyncResource }> = [
248
+ { path: '/v1/datasets', key: 'datasets', nullable: true, map: mapDataset },
249
+ { path: '/v1/connectors', key: 'connectors', nullable: false, map: mapConnector },
250
+ { path: '/v1/embed-jobs', key: 'embed_jobs', nullable: true, map: mapEmbedJob },
251
+ ];
252
+
253
+ /** Pull all modeled real collections via the executor and map them to twin sync resources. */
254
+ export async function pullCohereState(execute: CohereExecute): Promise<SyncResource[]> {
255
+ const out: SyncResource[] = [];
256
+ for (const c of COLLECTIONS) {
257
+ const res = await execute('GET', c.path);
258
+ throwIfError(res, `pull ${c.path}`);
259
+ for (const item of listOf(res, c.key, c.nullable, `pull ${c.path}`)) out.push(c.map(item));
260
+ }
261
+ return out;
262
+ }
263
+
264
+ /**
265
+ * A MOVING poll timestamp, forced strictly increasing within the process. NOT a pinned constant:
266
+ * the kernel hashes an observed event over (`occurredAt` + post-state), so under a fixed poll time
267
+ * a vendor value that REVERTS (a connector toggled active→inactive→active across polls) collides
268
+ * with its own earlier observation and `syncPull` reports `deltasAppended: 1` while nothing lands
269
+ * — a phantom delta, with the projection still serving the stale value.
270
+ */
271
+ let lastPoll = 0;
272
+ export function pollTimestamp(): string {
273
+ const now = Math.max(Date.now(), lastPoll + 1);
274
+ lastPoll = now;
275
+ return new Date(now).toISOString();
276
+ }
277
+
278
+ /**
279
+ * Pull from real Cohere and fold into the twin (mirror seeding). syncPull's shadow-diff makes a
280
+ * re-pull of identical state a no-op.
281
+ */
282
+ export async function syncCohereFromReal(
283
+ execute: CohereExecute,
284
+ opts: { root?: string; occurredAt?: string } = {},
285
+ ): Promise<{ observed: number; deltasAppended: number }> {
286
+ const resources = await pullCohereState(execute);
287
+ const result = syncPull({
288
+ service: SERVICE,
289
+ resources,
290
+ occurredAt: opts.occurredAt ?? pollTimestamp(),
291
+ ...(opts.root !== undefined ? { root: opts.root } : {}),
292
+ });
293
+ return { observed: result.observed, deltasAppended: result.deltasAppended };
294
+ }
295
+
296
+ // ── PUSH ────────────────────────────────────────────────────────────────────
297
+
298
+ /** The twin operations this connector knows how to push. Anything not here must FAIL LOUDLY
299
+ * rather than be silently dropped — pushing an unrecognized op risks hitting the wrong endpoint
300
+ * or no-op'ing a real change. */
301
+ const PUSHABLE_VERBS = new Set(['create', 'update', 'cancel', 'delete']);
302
+
303
+ /**
304
+ * Subject types NO verb of which can be pushed, keyed by TYPE rather than by operation.
305
+ *
306
+ * A type-wide rule is the only shape that cannot be out-flanked by a verb nobody thought of: an
307
+ * op-keyed list covering `token.observe` would let a hypothetical `token.delete` through, and the
308
+ * request builder's `/v1/${type}s` fallback would then fire `DELETE /v1/tokens/<id>` — an endpoint
309
+ * Cohere does not have — at a REAL account.
310
+ */
311
+ const UNPUSHABLE_TYPES: Record<string, string> = {
312
+ token: "the tokenizer vocabulary is twin-local state with no vendor endpoint in either direction — Cohere's tokenizer is fixed server-side, so there is nothing to push and nothing to pull",
313
+ };
314
+
315
+ /**
316
+ * Individual operations whose verb and type are otherwise pushable, but whose vendor request this
317
+ * connector cannot faithfully reproduce from stored state. Refusing them by name is the honest
318
+ * answer; pushing an approximation would write the wrong thing to a real account, which is worse
319
+ * than not pushing at all. Each is filed as a todo in the manifest.
320
+ */
321
+ const UNPUSHABLE_OPS: Record<string, string> = {
322
+ 'dataset.create': "the vendor's dataset create is a multipart/form-data upload whose `data` part is the dataset FILE, and whose name/type travel as query parameters — not the JSON body this executor sends, and the twin does not retain the original bytes; see cohere.connector.push_dataset_multipart",
323
+ };
324
+
325
+ /**
326
+ * Why this op is a KNOWN, filed, un-pushable gap — or undefined if the connector can push it.
327
+ *
328
+ * This answers only for gaps that have been REASONED ABOUT. An operation the connector has simply
329
+ * never heard of is NOT this function's business: it is a coding gap, and `assertPushable` throws
330
+ * on it, loudly. Routing every unknown verb through the skip-and-count path would turn a genuine
331
+ * unhandled write into an anonymous integer that ticks up and tells nobody.
332
+ */
333
+ export function unpushableReason(op: string, subjectType?: string): string | undefined {
334
+ if (subjectType !== undefined && UNPUSHABLE_TYPES[subjectType] !== undefined) return UNPUSHABLE_TYPES[subjectType];
335
+ return UNPUSHABLE_OPS[op];
336
+ }
337
+
338
+ /**
339
+ * Does this subject have a VENDOR identity, or only a twin-minted one?
340
+ *
341
+ * THE BUG THIS CLOSES (§9 round 1, connector B1 + M1 — one BLOCKER and one MAJOR, both the same
342
+ * root class). The twin mints UUID-SHAPED ids, so a twin id is indistinguishable from a real
343
+ * Cohere id in an operator's logs. Two ordinary sequences fired one at the REAL account:
344
+ *
345
+ * B1 create a connector, PATCH it, push. The create confirms under the vendor's id, but the
346
+ * QUEUED update still carries the twin-minted id, so `PATCH /v1/connectors/<twin-uuid>` went
347
+ * out. Real Cohere 404s, `throwIfError` aborts the batch mid-run, and the update stays
348
+ * PENDING — so every later push re-issues the same doomed request forever.
349
+ * M1 create a dataset, delete it, push. `dataset.create` is a FILED multipart gap, so it is
350
+ * skipped — and skipping it is exactly what made the delete reachable:
351
+ * `DELETE /v1/datasets/<twin-uuid>` went out. Aborting at the refused create would have
352
+ * stopped the run; the skip is what let it through. That is the "trace what a fix makes
353
+ * newly REACHABLE" class, and the fix's own reasoning had it backwards.
354
+ *
355
+ * The discriminator is the `_twin_minted` marker the handler stamps on every local create. A row
356
+ * that arrived through `syncPull` carries no marker and IS known to the vendor under its own id, so
357
+ * a mutation on it pushes normally — that path must keep working, which is why "does a local create
358
+ * action exist" is the question rather than "does the id look local".
359
+ */
360
+ type Identity =
361
+ | { kind: 'vendor' } // the vendor knows this id; push it as-is
362
+ | { kind: 'superseded'; externalId: string } // the vendor knows it under a DIFFERENT id
363
+ | { kind: 'local' }; // the vendor has never seen it
364
+
365
+ /**
366
+ * What identity does this subject have AT THE VENDOR? Read from the durable projection, so the
367
+ * answer survives a restart, an aborted batch and a fresh process.
368
+ *
369
+ * §9 ROUND TWO, BLOCKER. Round one fixed B1 with an in-loop `Map` and M1 with a `_twin_minted`
370
+ * marker — and the two fixes DESTROYED each other on the same state. `confirmAction` suppresses the
371
+ * confirmed create's whole projection, and the supersede tombstone it writes carries only
372
+ * `{_deleted, _superseded_by}` — so `_twin_minted`, which lived on the now-suppressed create,
373
+ * VANISHES. The local id then looked vendor-known, and the per-batch map was the only thing left
374
+ * holding the line:
375
+ *
376
+ * create + PATCH queued → push #1 confirms the create, then the PATCH throws (a budget refusal
377
+ * mid-batch, a retry-after past the cap, a network error, any vendor `{message}`) → batch
378
+ * aborts, the PATCH stays pending → push #2 has an EMPTY map and a marker-less tombstone
379
+ * → `PATCH /v1/connectors/<twin-uuid>` at the REAL account, every run, forever.
380
+ *
381
+ * That is byte-for-byte the outcome B1 claimed to close, and neither the capability written to
382
+ * prove B1 nor its revert-matrix cell could see it — both are single-batch by construction.
383
+ *
384
+ * The tombstone the confirm already writes IS the durable mapping; reading it is what makes the
385
+ * resolution survive the batch. `mintedExternal` is now only a within-run cache for rows confirmed
386
+ * moments ago, whose tombstone this same call would find anyway.
387
+ */
388
+ function vendorIdentity(subjectType: string, subjectId: string, root?: string): Identity {
389
+ const row = projectResources(SERVICE, root).find((r) => r.type === subjectType && r.id === subjectId);
390
+ // No row at all: nothing local ever claimed this id, so it can only have come from the vendor.
391
+ if (row === undefined) return { kind: 'vendor' };
392
+ // A supersede tombstone records exactly which vendor id this local id became.
393
+ if (typeof row._superseded_by === 'string' && row._superseded_by) return { kind: 'superseded', externalId: row._superseded_by };
394
+ if (row._twin_minted === true) return { kind: 'local' };
395
+ return { kind: 'vendor' };
396
+ }
397
+
398
+ /** Fields to CONFIRM with — the twin's own bookkeeping removed. `_twin_minted` must not ride along
399
+ * to the vendor-id row: that row IS vendor-known, and leaving the marker on it would make every
400
+ * later mutation of a converged resource refuse itself. */
401
+ function confirmFields(fields: Record<string, unknown> | undefined): Record<string, unknown> {
402
+ const out: Record<string, unknown> = { ...(fields ?? {}) };
403
+ delete out._twin_minted;
404
+ return out;
405
+ }
406
+
407
+
408
+ /**
409
+ * Resolve every id a payload REFERENCES, by the same rule the subject gets.
410
+ *
411
+ * The only foreign key this twin pushes is an embed job's `input_dataset_id`. It matters because
412
+ * `dataset.create` is an un-pushable filed gap, so a locally created dataset is PERMANENTLY
413
+ * vendor-unknown — and the embed-job create would then have posted
414
+ * `{"dataset_id": "<twin-uuid>"}` to the real account (§9 round two, MAJOR). Worse, the capability
415
+ * written to pin the read-shape→write-shape rename ASSERTED that it did.
416
+ */
417
+ function resolveForeignKeys<A extends Pick<TwinAction, 'operation' | 'subject' | 'fields'>>(
418
+ action: A,
419
+ cache: Map<string, string>,
420
+ root?: string,
421
+ ): { action: A; refusal?: string } {
422
+ const op = action.operation ?? '';
423
+ if (op !== 'embed_job.create') return { action };
424
+ const datasetId = (action.fields ?? {}).input_dataset_id;
425
+ if (typeof datasetId !== 'string' || datasetId === '') return { action };
426
+ const cached = cache.get(`dataset:${datasetId}`);
427
+ const identity: Identity = cached !== undefined ? { kind: 'superseded', externalId: cached } : vendorIdentity('dataset', datasetId, root);
428
+ if (identity.kind === 'local') {
429
+ return {
430
+ action,
431
+ refusal: `the embed job references dataset '${datasetId}', which this twin minted and the vendor has never seen (dataset creates are an un-pushable multipart gap) — posting that id would ask the real account to embed a dataset that does not exist there`,
432
+ };
433
+ }
434
+ if (identity.kind === 'superseded') {
435
+ return { action: { ...action, fields: { ...(action.fields ?? {}), input_dataset_id: identity.externalId } } };
436
+ }
437
+ return { action };
438
+ }
439
+
440
+ /** Throw if `op` is not a write operation this connector can faithfully push. */
441
+ function assertPushable(op: string, subjectType?: string): void {
442
+ // NAMED GAP FIRST, so a filed, reasoned gap always answers with ITS reason rather than the
443
+ // generic "unsupported operation" — otherwise a caller cannot tell a thought-about gap from a
444
+ // hole nobody has looked at, which is the whole distinction this pair encodes.
445
+ const why = unpushableReason(op, subjectType);
446
+ if (why !== undefined) throw new Error(`cohere push: cannot faithfully push '${op}' — ${why}`);
447
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
448
+ if (!PUSHABLE_VERBS.has(verb)) {
449
+ throw new Error(`cohere push: unsupported operation '${op}' — refusing to silently drop a local write`);
450
+ }
451
+ }
452
+
453
+ /** Map a subject type → its REST collection path. */
454
+ const COLLECTION_PATH: Record<string, string> = {
455
+ dataset: '/v1/datasets',
456
+ connector: '/v1/connectors',
457
+ embed_job: '/v1/embed-jobs',
458
+ };
459
+
460
+ /**
461
+ * Resolve the REST (method, path) for ONE pending action — faithful to the real Cohere REST
462
+ * surface for every write op the twin records:
463
+ * - <type>.create → POST <collection>
464
+ * - <type>.update → PATCH <collection>/:id
465
+ * - <type>.cancel → POST <collection>/:id/cancel
466
+ * - <type>.delete → DELETE <collection>/:id
467
+ */
468
+ export function cohereRequestForAction(action: Pick<TwinAction, 'operation' | 'subject'>): { method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; path: string } {
469
+ const op = action.operation ?? `${action.subject.type}.update`;
470
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
471
+ const collection = COLLECTION_PATH[action.subject.type] ?? `/v1/${action.subject.type}s`;
472
+ if (verb === 'create') return { method: 'POST', path: collection };
473
+ if (verb === 'update') return { method: 'PATCH', path: `${collection}/${action.subject.id}` };
474
+ if (verb === 'cancel') return { method: 'POST', path: `${collection}/${action.subject.id}/cancel` };
475
+ if (verb === 'delete') return { method: 'DELETE', path: `${collection}/${action.subject.id}` };
476
+ // assertPushable rejects anything else, so this is only reached for the pushable verbs above.
477
+ return { method: 'POST', path: collection };
478
+ }
479
+
480
+ /** The vendor-accepted payload for a create/update. The stored fields include derived lifecycle
481
+ * data the endpoints do not accept, so the twin's own bookkeeping is stripped. */
482
+ function payloadFor(action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>, verb: string): Record<string, unknown> | undefined {
483
+ const f = (action.fields ?? {}) as Record<string, unknown>;
484
+ if (verb === 'update') {
485
+ // `UpdateConnectorRequest` is a strict subset: name / url / excludes / active /
486
+ // continue_on_failure. Replaying `auth_status` or `created_at` would be sending the vendor
487
+ // fields its own PATCH schema does not declare.
488
+ const out: Record<string, unknown> = {};
489
+ for (const k of ['name', 'url', 'excludes', 'active', 'continue_on_failure'] as const) {
490
+ if (f[k] !== undefined) out[k] = f[k];
491
+ }
492
+ return out;
493
+ }
494
+ if (verb !== 'create') return undefined;
495
+ switch (action.subject.type) {
496
+ case 'connector':
497
+ // `CreateConnectorRequest` REQUIRES name + url; the rest are optional. `auth_type` and
498
+ // `auth_status` are DERIVED server-side and are not accepted on create.
499
+ return {
500
+ name: f.name,
501
+ url: f.url,
502
+ ...(f.description ? { description: f.description } : {}),
503
+ ...(Array.isArray(f.excludes) && f.excludes.length ? { excludes: f.excludes } : {}),
504
+ ...(f.oauth ? { oauth: f.oauth } : {}),
505
+ ...(f.active !== undefined ? { active: f.active } : {}),
506
+ ...(f.continue_on_failure !== undefined ? { continue_on_failure: f.continue_on_failure } : {}),
507
+ };
508
+ case 'embed_job':
509
+ // `CreateEmbedJobRequest` REQUIRES model + dataset_id + input_type. The twin stores the
510
+ // dataset reference as `input_dataset_id` (the READ shape) and the create endpoint wants
511
+ // `dataset_id` (the WRITE shape) — the two names differ on the vendor's own schemas.
512
+ //
513
+ // `dataset_id` is resolved by the caller BEFORE this runs (§9 round two, MAJOR): the
514
+ // identity guard covers `subject.id`, and a FOREIGN KEY is just as capable of carrying a
515
+ // twin-minted UUID to the real account. See `resolveForeignKeys`.
516
+ return {
517
+ model: f.model,
518
+ dataset_id: f.input_dataset_id,
519
+ input_type: f.input_type ?? 'search_document',
520
+ ...(f.name ? { name: f.name } : {}),
521
+ ...(f.truncate ? { truncate: f.truncate } : {}),
522
+ };
523
+ // `dataset.create` is refused by `assertPushable` above (multipart), so it has no payload.
524
+ default:
525
+ return {};
526
+ }
527
+ }
528
+
529
+ /**
530
+ * The id the VENDOR minted, read out of the create response.
531
+ *
532
+ * Cohere's create responses do NOT share one envelope, and a generic `res.id` would silently fall
533
+ * back to the twin's local id on two of the three types — which is exactly the shape that makes a
534
+ * push-then-pull serve one account row TWICE:
535
+ * • dataset → `{ id }` (`DatasetsCreateResponse`)
536
+ * • connector → `{ connector: { id } }` (`CreateConnectorResponse` — NESTED)
537
+ * • embed job → `{ job_id }` (`CreateEmbedJobResponse` — a different key)
538
+ */
539
+ function externalIdOf(subjectType: string, res: unknown): string {
540
+ const o = (res ?? {}) as Record<string, unknown>;
541
+ const found = subjectType === 'connector'
542
+ ? (o.connector as { id?: unknown } | undefined)?.id
543
+ : subjectType === 'embed_job'
544
+ ? o.job_id
545
+ : o.id;
546
+ if (typeof found === 'string' && found) return found;
547
+ // NO FALLBACK TO THE LOCAL ID (§9 round two, m-R2.1). Confirming under the twin's own UUID would
548
+ // MANUFACTURE a vendor identity: no tombstone is written, so `vendorIdentity` would afterwards
549
+ // report that id as vendor-known and every later mutation would push the twin's UUID at the real
550
+ // account. A create response that carries no id is a malformed response, not a successful create.
551
+ throw new Error(`cohere push: the ${subjectType} create response carried no vendor id — refusing to confirm under the twin's own id`);
552
+ }
553
+
554
+ /**
555
+ * Push ONE pending action to REAL Cohere via the injected executor. Returns the real external id.
556
+ * WRITES TO THE REAL ACCOUNT.
557
+ */
558
+ export async function pushCohereAction(
559
+ execute: CohereExecute,
560
+ action: Pick<TwinAction, 'operation' | 'subject' | 'fields'>,
561
+ ): Promise<{ externalId: string }> {
562
+ const op = action.operation ?? `${action.subject.type}.update`;
563
+ assertPushable(op, action.subject.type);
564
+ const { method, path } = cohereRequestForAction(action);
565
+ const verb = op.includes('.') ? op.slice(op.indexOf('.') + 1) : op;
566
+ const payload = payloadFor(action, verb);
567
+ const res = await execute(method, path, payload);
568
+ throwIfError(res, `push ${action.subject.type}`);
569
+ return { externalId: verb === 'create' ? externalIdOf(action.subject.type, res) : action.subject.id };
570
+ }
571
+
572
+ /**
573
+ * Push the twin's PENDING local actions to real Cohere and CONFIRM each: for every pending action,
574
+ * call the real API; on success, confirmAction records the confirmed fields as an observed event
575
+ * and maps action → event (suppressing the local projection — counted exactly once). Idempotency:
576
+ * a confirmed action is no longer pending, so a re-push enacts NOTHING.
577
+ *
578
+ * COMPOUND-ACTION AUDIT (the `confirmAction` hazard in ADDING_A_TWIN.md §5): every action this
579
+ * connector pushes is SINGLE-RESOURCE — a connector or embed-job create/update, an embed-job
580
+ * cancel, a dataset or connector delete — so `fields` alone is the complete post-state and no
581
+ * `additionalObservations` are needed for the payload. The one write this twin makes that touches
582
+ * several resources at once is a TOKENIZE (one `token.observe` per fresh segment) — and those are
583
+ * separate single-resource actions on a type that is refused push-wide, so the hazard cannot
584
+ * arise here.
585
+ */
586
+ export async function pushPendingCohereActions(
587
+ execute: CohereExecute,
588
+ opts: { root?: string; occurredAt: string },
589
+ ): Promise<{ pushed: number; confirmed: string[]; externalIds: Record<string, string>; refused: Array<{ actionId: string; operation: string; subjectId: string; reason: string }> }> {
590
+ const confirmed: string[] = [];
591
+ const externalIds: Record<string, string> = {};
592
+ const refused: Array<{ actionId: string; operation: string; subjectId: string; reason: string }> = [];
593
+ // Vendor ids minted DURING this run, keyed by the local subject they replaced. A create confirms
594
+ // under the vendor's id; every later action in the same batch that names the superseded local id
595
+ // must be rewritten through this map before its request is built.
596
+ const mintedExternal = new Map<string, string>();
597
+ const subjectKey = (type: string, id: string) => `${type}:${id}`;
598
+
599
+ for (const pending of pendingActions(SERVICE, opts.root)) {
600
+ const operation = pending.operation ?? `${pending.subject.type}.update`;
601
+ const verb = operation.includes('.') ? operation.slice(operation.indexOf('.') + 1) : operation;
602
+ // A KNOWN, filed gap is REPORTED BY NAME and skipped: throwing would abort the batch and
603
+ // leave the account half-pushed over a write we already decided not to send, while dropping
604
+ // it silently is what the doctrine forbids. Anything NOT on that list falls through to
605
+ // `pushCohereAction` → `assertPushable`, which THROWS — an operation nobody has reasoned about
606
+ // must stop the run, not become an anonymous tally.
607
+ const why = unpushableReason(operation, pending.subject.type);
608
+ if (why !== undefined) {
609
+ refused.push({ actionId: pending.id, operation, subjectId: pending.subject.id, reason: why });
610
+ continue;
611
+ }
612
+
613
+ // IDENTITY RESOLUTION, before any request is built (§9 round 1 B1/M1, hardened in round two).
614
+ // A non-create acts on a subject that must already exist AT THE VENDOR:
615
+ // • superseded — a create already mapped this local id to a vendor id (this run's cache, or
616
+ // the DURABLE supersede tombstone from any earlier run): address the vendor's id;
617
+ // • local — the subject is twin-minted and was never successfully pushed, so there is NOTHING
618
+ // at the vendor to act on. Sending the twin's own UUID would mutate or delete whatever (if
619
+ // anything) carries that id in the real account. Refuse BY NAME, so it is visible every run;
620
+ // • vendor — the id came from a pull and is the vendor's own: push it as-is.
621
+ let action: Pick<TwinAction, 'id' | 'operation' | 'subject' | 'fields'> = pending;
622
+ if (verb !== 'create') {
623
+ const cached = mintedExternal.get(subjectKey(pending.subject.type, pending.subject.id));
624
+ const identity: Identity = cached !== undefined
625
+ ? { kind: 'superseded', externalId: cached }
626
+ : vendorIdentity(pending.subject.type, pending.subject.id, opts.root);
627
+ if (identity.kind === 'local') {
628
+ refused.push({
629
+ actionId: pending.id,
630
+ operation,
631
+ subjectId: pending.subject.id,
632
+ reason: `the subject was minted by this twin and the vendor has never seen it (its create was refused or never pushed), so '${operation}' has nothing to act on — sending the twin's own id would address an unrelated resource in the real account`,
633
+ });
634
+ continue;
635
+ }
636
+ if (identity.kind === 'superseded') {
637
+ action = { ...pending, subject: { ...pending.subject, id: identity.externalId } };
638
+ }
639
+ }
640
+
641
+ // FOREIGN KEYS are resolved by the SAME rule as the subject (§9 round two, MAJOR). The subject
642
+ // guard above says nothing about the ids a payload REFERENCES, and an embed-job create carries
643
+ // its dataset's id in the body — so a locally-minted dataset id sailed straight to the real
644
+ // account, where the vendor rejects it and the batch wedges exactly as B1 did.
645
+ const fk = resolveForeignKeys(action, mintedExternal, opts.root);
646
+ if (fk.refusal !== undefined) {
647
+ refused.push({ actionId: pending.id, operation, subjectId: pending.subject.id, reason: fk.refusal });
648
+ continue;
649
+ }
650
+ action = fk.action;
651
+
652
+ const { externalId } = await pushCohereAction(execute, action);
653
+ // CONFIRM UNDER THE VENDOR'S ID, not the twin's locally-minted one. The local mint is a
654
+ // placeholder that only ever existed here; the account knows the row by the id its create
655
+ // response returned. Confirming under the local id would leave the local projection standing
656
+ // while the next `pullCohereState` folded the SAME resource in under its real id — one account
657
+ // row served as TWO. Re-subjecting the confirm suppresses the local action's projection and
658
+ // rebuilds the row at the real id, so push-then-pull converges.
659
+ const subject = { ...action.subject, id: externalId };
660
+ // TOMBSTONE THE SUPERSEDED LOCAL ID, IN THE SAME CONFIRM. Re-subjecting suppresses the local
661
+ // action's projection entirely, so without this the placeholder id would leave the row set with
662
+ // NO record that it ever existed or what it became — a client still holding it gets a bare 404
663
+ // and no way to follow the resource to its real id. The tombstone keeps that provenance
664
+ // (`_superseded_by`) auditable and the id visibly occupied.
665
+ //
666
+ // HONEST SCOPE (§9 round 1, m4, corrected by its own revert-matrix cell): this does NOT stop
667
+ // `nextId` re-issuing the id. That mint takes its ordinal from the row-set SIZE and only ever
668
+ // probes upward, so a superseded id is not a re-issue candidate with or without the tombstone.
669
+ // An earlier version of this note claimed otherwise, and the pin written to prove that claim
670
+ // came out hollow.
671
+ //
672
+ // It rides `additionalObservations` — the
673
+ // kernel's own seam for a compound confirm — rather than a second `applyTwinWrite`: a local
674
+ // write here would itself be a PENDING action, which the very next push would try to send to
675
+ // the vendor as `<type>.superseded`.
676
+ const tombstone = externalId !== action.subject.id
677
+ ? [{ subject: action.subject, fields: { _deleted: true, _superseded_by: externalId } }]
678
+ : [];
679
+ if (externalId !== action.subject.id) mintedExternal.set(subjectKey(action.subject.type, action.subject.id), externalId);
680
+ confirmAction({
681
+ service: SERVICE, actionId: action.id, subject, fields: confirmFields(action.fields),
682
+ ...(tombstone.length ? { additionalObservations: tombstone } : {}),
683
+ occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}),
684
+ });
685
+ confirmed.push(action.id);
686
+ externalIds[action.id] = externalId;
687
+ }
688
+ return { pushed: confirmed.length, confirmed, externalIds, refused };
689
+ }
690
+
691
+ // ── FULL bi-directional sync ─────────────────────────────────────────────────
692
+ /**
693
+ * FULL bi-directional sync over the injected client: (1) PUSH every pending local action to real
694
+ * Cohere and confirm it, then (2) PULL all modeled collections back and fold them into the event
695
+ * log. Pushing first means the pull observes the twin's own writes as confirmed external state (no
696
+ * double-count). Re-running with no pending writes and identical real state is a no-op (push 0,
697
+ * deltasAppended 0). Same code path offline (fake executor) and live (real key).
698
+ */
699
+ export async function fullSyncCohere(
700
+ execute: CohereExecute,
701
+ opts: { root?: string; occurredAt: string },
702
+ ): Promise<{ pushed: number; refused: Array<{ actionId: string; operation: string; subjectId: string; reason: string }>; observed: number; deltasAppended: number; collections: number }> {
703
+ const push = await pushPendingCohereActions(execute, { occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
704
+ const resources = await pullCohereState(execute);
705
+ const pull = syncPull({ service: SERVICE, resources, occurredAt: opts.occurredAt, ...(opts.root !== undefined ? { root: opts.root } : {}) });
706
+ // The REFUSALS THEMSELVES, not a count: a bare integer that ticks up and never comes down is not
707
+ // a signal an operator can act on.
708
+ return { pushed: push.pushed, refused: push.refused, observed: pull.observed, deltasAppended: pull.deltasAppended, collections: COLLECTIONS.length };
709
+ }