@opencxh/domain 1.127.0 → 1.130.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.
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,50 @@
1
+ import { OwnerScope } from '../contact/types';
2
+ /** Where a company row came from. `local` is a row somebody created by hand. */
3
+ export interface CompanySource {
4
+ providerId: string;
5
+ readOnly?: boolean;
6
+ }
7
+ /**
8
+ * A company we own a row for.
9
+ *
10
+ * The identity fields are deliberately flat string arrays: only that shape is
11
+ * indexable in this datastore, so `keys`, `domains` and `externalIds` are what makes a
12
+ * lookup an indexed exact match instead of a scan.
13
+ */
14
+ export interface Company {
15
+ id: string;
16
+ organizationId: string;
17
+ name: string;
18
+ /**
19
+ * Registrable e-mail domains that identify this company (`vandijck.nl`). Matched
20
+ * after `registrableDomain`, so a subdomain resolves too. Public/consumer domains
21
+ * are rejected on write — one company with `gmail.com` here would claim every
22
+ * private customer.
23
+ */
24
+ domains: string[];
25
+ /**
26
+ * Canonical endpoint keys (`tel:+31882112121`, `mailto:info@vandijck.nl`), produced
27
+ * by `endpointKey`. Never a raw value: the raw form is kept for display only.
28
+ */
29
+ keys: string[];
30
+ /**
31
+ * Namespaced references into external systems (`hubspot:5591`, `exact:REL0042`).
32
+ * A reference, never a copy — live figures (balance, deal value) stay where they
33
+ * belong and are fetched on demand.
34
+ */
35
+ externalIds: string[];
36
+ ownerScope: OwnerScope;
37
+ source: CompanySource;
38
+ /** Raw, human-readable phone/website as entered, for display. */
39
+ phone?: string;
40
+ website?: string;
41
+ notes?: string;
42
+ }
43
+ /** What a resolve returns: which company, and how we got there. */
44
+ export interface CompanyMatch {
45
+ company: Company;
46
+ /** Which rung of the local chain matched — useful for debugging a wrong link. */
47
+ via: "contact" | "key" | "domain";
48
+ /** The contact row that carried the link, when `via` is `contact`. */
49
+ contactId?: string;
50
+ }
@@ -20,6 +20,16 @@ export interface ContactSource {
20
20
  externalId?: string;
21
21
  readOnly?: boolean;
22
22
  }
23
+ /**
24
+ * Where a contact row came from.
25
+ *
26
+ * - `own` — somebody created or promoted it here; freely editable.
27
+ * - `shadow` — a lazily cached hit from a federated source (a directory, a CRM). It
28
+ * exists so the second call from that number is an indexed hit instead of another
29
+ * fan-out. Hidden from the contact list by default: the Entra directory is full of
30
+ * your own colleagues, and writing those in as customers pollutes the list.
31
+ */
32
+ export type ContactOrigin = "own" | "shadow";
23
33
  export interface Contact {
24
34
  id: string;
25
35
  organizationId: string;
@@ -27,7 +37,26 @@ export interface Contact {
27
37
  source: ContactSource;
28
38
  firstName: string;
29
39
  lastName?: string;
40
+ /**
41
+ * Free-text company name. Stays as the display fallback for contacts that have no
42
+ * `companyId` — a dozen render sites use it as a label.
43
+ */
30
44
  company?: string;
45
+ /** The company row this contact belongs to, once resolved. */
46
+ companyId?: string;
31
47
  address?: string;
48
+ /** Display form: the resource exactly as entered or as the provider delivered it. */
32
49
  endpoints?: ContactEndpoint[];
50
+ /**
51
+ * Canonical, indexable mirror of `endpoints`, produced by `endpointKey` on every
52
+ * write. `endpoints[]` is an array of objects and therefore not indexable, which is
53
+ * why the old lookup was an unindexed `$elemMatch` scan. Read and write side must use
54
+ * the same normaliser or the index is structurally a miss.
55
+ */
56
+ keys?: string[];
57
+ /** Namespaced source references (`ms:AAMk…`, `google:people/c123`, `hubspot:42`). */
58
+ externalIds?: string[];
59
+ origin?: ContactOrigin;
60
+ /** When a shadow row last saw its source. There is no refresh job; touching refreshes. */
61
+ syncedAt?: number;
33
62
  }
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Which sources this organisation uses to search for and enrich contacts.
3
+ *
4
+ * Named `...Config` because `ContactSource` already means something else on a contact: its
5
+ * provenance (`{ providerId, externalId, readOnly }`). This is the register row.
6
+ *
7
+ * The one configuration surface of the contact layer. Deliberately *not* a tool name
8
+ * inside a playbook step: MCP tool names are `mcp__<server row id>__<tool>` and therefore
9
+ * per-organisation random, a shipped step could never name one, and playbook steps are
10
+ * snapshotted onto runs so changing a source would leave parked runs on the old one.
11
+ *
12
+ * Note what is NOT configurable here: personal versus organisation-wide. That follows from
13
+ * *who is asking* — an interactive search runs with the user's delegated tokens and may use
14
+ * their personal connections, an unattended job has no user and cannot, both because
15
+ * `SYSTEM_TOOL_POLICY` filters personal tools and because the Graph API refuses `/me/*`
16
+ * without a user token.
17
+ */
18
+ export interface ContactSourceConfig {
19
+ /**
20
+ * Either a built-in source id (`microsoft-directory`, `google`, `eylo-voip`, `local`)
21
+ * or an AI tool name (`mcp__<id>__search_companies`).
22
+ *
23
+ * A field VALUE, never an object key: the SDK client deep-mangles object keys between
24
+ * camel and snake case, which would corrupt any namespaced name used as a key.
25
+ */
26
+ sourceId: string;
27
+ kind: "internal" | "mcp";
28
+ /** Take part in the interactive launcher / composer search. */
29
+ search: boolean;
30
+ /** Take part in the background enrichment of interactions. */
31
+ enrich: boolean;
32
+ order: number;
33
+ }
34
+ /** A source as the settings page sees it: config merged with what is actually available. */
35
+ export interface ContactSourceEntry extends ContactSourceConfig {
36
+ label: string;
37
+ /** `platform` = shipped default, `org` = the organisation added it. */
38
+ origin: "platform" | "org";
39
+ /**
40
+ * True when the source is configured but its tool is gone — an MCP server that was
41
+ * disconnected. Surfaced rather than silently skipped, because a source that quietly
42
+ * stops answering looks exactly like a customer who is not in the CRM.
43
+ */
44
+ broken?: boolean;
45
+ }
46
+ export interface ContactSourceCatalog {
47
+ sources: ContactSourceEntry[];
48
+ /** Master switch for background enrichment. Absent means on. */
49
+ enrichmentEnabled: boolean;
50
+ /**
51
+ * Bumped on every change to the register. Used as the generation token of the negative
52
+ * cache, so a number that was unknown before gets a fresh chance once the organisation
53
+ * connects a new source — expiry on time alone would never do that.
54
+ */
55
+ generation: string;
56
+ }
@@ -80,6 +80,21 @@ export interface Interaction {
80
80
  * en meetings hier de volledige lijst inclusief rollen (cc/bcc/organizer/…).
81
81
  */
82
82
  participants?: InteractionParticipant[];
83
+ /**
84
+ * Canonieke identiteitssleutels van de tegenpartij — de andere helft van `channelUriKey`,
85
+ * dat onze eigen kant canonicaliseert. Geïndexeerd, zodat "elk gesprek met dit adres, dit
86
+ * nummer of dit domein" een indexvraag is en geen scan.
87
+ *
88
+ * Bevat `mailto:`/`tel:`-sleutels, een `domain:<registreerbaar>`-pseudosleutel voor
89
+ * niet-publieke e-maildomeinen, en `company:<id>`/`contact:<id>` wanneer een mens een
90
+ * koppeling expliciet heeft gelegd.
91
+ *
92
+ * **Een sleutel wordt nooit onwaar.** "Dit adres kwam voor in dit gesprek" blijft altijd
93
+ * gelden, terwijl een afgeleide `companyId` onwaar wordt zodra iemand een bedrijf aanmaakt,
94
+ * samenvoegt of corrigeert — en dan backfill zou vragen. Los een identiteit bij het lezen op
95
+ * naar zijn sleutels en het antwoord omvat ook gesprekken van vóór die identiteit bestond.
96
+ */
97
+ partyKeys?: string[];
83
98
  source?: InteractionSource;
84
99
  tags: string[];
85
100
  links: InteractionLink[];
@@ -0,0 +1,19 @@
1
+ import { MemorySubjectKey } from './item';
2
+ /**
3
+ * Twee subjects die hetzelfde blijken te zijn (een merge van interacties, later een echte
4
+ * identiteitslaag). Het geheugen verhuist niet: de alias wordt bij het lezen opgelost.
5
+ *
6
+ * **Eén niveau diep by construction**: een alias wiens `canonical` zelf een alias is wordt
7
+ * geweigerd bij het schrijven. Resolutie is daarmee twee queries en nooit recursief — geen
8
+ * cyclus-detectie, geen diepte-limiet, geen verrassing op het hete pad.
9
+ */
10
+ export interface MemorySubjectAlias {
11
+ id: string;
12
+ organizationId: string;
13
+ /** Het subject dat is opgegaan in `canonical`. Unique per org. */
14
+ subject: MemorySubjectKey;
15
+ canonical: MemorySubjectKey;
16
+ /** De app die de samenvoeging meldde. */
17
+ source: string;
18
+ createdAt?: number;
19
+ }
@@ -0,0 +1,5 @@
1
+ export * from './alias';
2
+ export * from './ingest';
3
+ export * from './item';
4
+ export * from './kind';
5
+ export * from './query';
@@ -0,0 +1,61 @@
1
+ import { MemoryKindId, MemoryLink, MemorySubjectKey } from './item';
2
+ /**
3
+ * Schrijven is een **CAS-upsert**, geen snapshot-replace zoals analytics.
4
+ *
5
+ * Analytics mag een hele periode weggooien en opnieuw wegschrijven omdat een fact
6
+ * herberekenbaar is uit de bronstore. Een delta-gevouwen verhaal is de opgetelde uitkomst
7
+ * van N modelcalls en is dat niet. Bovendien is remove+insert niet atomair: een lezer kan
8
+ * "geen item" observeren op een subject dat wél een verhaal heeft.
9
+ */
10
+ export interface MemoryUpsertRequest {
11
+ organizationId: string;
12
+ /** De schrijvende app; de hub vult dit uit `req.source.app` en vertrouwt de body hier niet. */
13
+ source: string;
14
+ subject: MemorySubjectKey;
15
+ kind: MemoryKindId;
16
+ /** Default bij `cardinality: "single"`: `"<kind>:<subject>"`. */
17
+ sourceRef?: string;
18
+ title: string;
19
+ body: string;
20
+ occurredAt?: number;
21
+ /** Watermerk van het bron-event dat deze schrijfactie veroorzaakte. */
22
+ throughRef?: string;
23
+ throughAt?: number;
24
+ /** Mismatch => `accepted: false` met `reason: "stale-version"`; de caller herplant. */
25
+ expectedVersion?: number;
26
+ audienceRef?: string;
27
+ locale?: string;
28
+ /** Weglaten: de hub extraheert keywords zelf uit titel + body (één tokenizer, schrijf én lees). */
29
+ keywords?: string[];
30
+ tags?: string[];
31
+ links?: MemoryLink[];
32
+ }
33
+ export type MemoryUpsertRejection = "stale-version" | "stale-watermark" | "unknown-kind" | "kind-disabled" | "subject-kind-mismatch" | "not-owner";
34
+ export interface MemoryUpsertResult {
35
+ accepted: boolean;
36
+ /** De versie zoals die nu in de store staat — ook bij een afwijzing, zodat de caller kan herplannen. */
37
+ version: number;
38
+ itemId?: string;
39
+ reason?: MemoryUpsertRejection;
40
+ }
41
+ /**
42
+ * Expliciet destructief: gooit alle items van (subject, kind) van deze bron weg en zet
43
+ * `items` ervoor terug. Alleen voor **herberekenbare** kinds, bv. een nachtelijke re-fold.
44
+ */
45
+ export interface MemoryReplaceRequest {
46
+ organizationId: string;
47
+ source: string;
48
+ subject: MemorySubjectKey;
49
+ kind: MemoryKindId;
50
+ items: MemoryUpsertRequest[];
51
+ }
52
+ /**
53
+ * Bron-gestuurd wissen. De **bron** pusht dit; de hub interpreteert geen deletes: de
54
+ * mapping van een verwijderd contact naar zijn interacties leeft in comms, en
55
+ * `contacts:deleted` publiceert alleen `{ id }` — zonder `organizationId`.
56
+ */
57
+ export interface MemoryPurgeRequest {
58
+ organizationId: string;
59
+ source: string;
60
+ subjects: MemorySubjectKey[];
61
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * De geheugenlaag (`apps/context`): één durende laag per **subject**, waarbij de
3
+ * organisatie zelf per event bepaalt wat er onthouden wordt.
4
+ *
5
+ * `Memory*` en niet `Context*`: `platform/context.ts` exporteert al `HostContext` en
6
+ * `context.collect` (vluchtige route-context) uit dezelfde platte barrel.
7
+ */
8
+ /** Een bestaande scopeKey: `interaction:i_1`, `contact:c_9`. Een latere identiteitslaag is gewoon een ander prefix. */
9
+ export type MemorySubjectKey = string;
10
+ /** `comms.resolution` (app-gedeclareerd) of `sales_besluitvormer` (org-eigen). */
11
+ export type MemoryKindId = string;
12
+ /**
13
+ * Een verwijzing naar iets anders — intern (`interaction:i_2`) of extern
14
+ * (`hubspot:5591`, `clickup:86ab2`).
15
+ *
16
+ * Een **lijst**, nooit een object met de relatie als key: de app-sdk http-client
17
+ * mangelt elke object-key camel<->snake, dus `{ "crm.company": "..." }` komt verminkt
18
+ * aan de andere kant aan.
19
+ */
20
+ export interface MemoryLink {
21
+ rel: string;
22
+ target: string;
23
+ }
24
+ /** Harde grens op de body. Geheugen is een samenvatting; een mailbox-schaduwkopie is het niet. */
25
+ export declare const MAX_MEMORY_CHARS = 2000;
26
+ export interface MemoryItem {
27
+ id: string;
28
+ organizationId: string;
29
+ subject: MemorySubjectKey;
30
+ /**
31
+ * De dossiersleutels waaronder dit item vindbaar is: het subject zelf, plus de sleutels die
32
+ * de eigenaar-app van dat subject teruggaf (`ScopeAuth.keys`). Gestempeld bij het schrijven.
33
+ *
34
+ * Dit is wat "vertel me wat er speelde bij deze klant" één geïndexeerde query maakt in
35
+ * plaats van een traversal. Een casus die op een gesprek is onthouden draagt via
36
+ * `Interaction.partyKeys` ook `domain:vandijck.nl` en `company:co_1`; het bedrijfsdossier
37
+ * matcht daarop met zijn eigen handvol sleutels, hoeveel medewerkers dat bedrijf ook heeft.
38
+ *
39
+ * **`keys` en niet `subjects`**, want dat zijn ze niet: `domain:vandijck.nl` is geen subject
40
+ * — geen app bezit die soort, dus `authorizeIdentity` erop geeft `allowed: false`. Een naam
41
+ * die belooft dat dit subjects zijn, nodigt precies die fout uit. Zelfde woord als
42
+ * `Interaction.partyKeys`, `Contact.keys` en `ScopeAuth.keys`: één begrip, één term.
43
+ *
44
+ * Bewust een momentopname en niet op leesmoment opgelost: opgelost bij elke query zou
45
+ * betekenen dat elke hit een autorisatie-fan-out kost — precies wat de engine zich niet kan
46
+ * veroorloven. De prijs is dat een sleutel die *later* wordt vastgelegd dit item niet meer
47
+ * bereikt; in de praktijk dekken `mailto:`/`domain:` dat vrijwel altijd al.
48
+ *
49
+ * `subject` blijft het anker, en is dus géén element-onder-de-andere: de unieke index, het
50
+ * vouwen, het wissen bij een verwijderd contact en de autorisatie lopen alle vier dáárover,
51
+ * en die hebben er exact één nodig.
52
+ */
53
+ keys?: string[];
54
+ kind: MemoryKindId;
55
+ title: string;
56
+ /** DE inhoud: markdown, begrensd op {@link MAX_MEMORY_CHARS}. Nooit een genest object. */
57
+ body: string;
58
+ /** Schrijvende app (== `req.source.app`); playbook-schrijfacties komen binnen als "ai". */
59
+ source: string;
60
+ /** Identiteit binnen (subject, kind) voor de bron. Bij `cardinality: "single"` de vouw-sleutel. */
61
+ sourceRef: string;
62
+ occurredAt: number;
63
+ /**
64
+ * Watermerk: tot en met welk bron-event dit item gevouwen is. Maakt dubbele events en
65
+ * retries gratis — een oudere `throughAt` wordt afgewezen in plaats van overschreven.
66
+ */
67
+ throughRef?: string;
68
+ throughAt?: number;
69
+ /** Optimistic concurrency; de hub doet compare-and-swap op dit veld. */
70
+ version: number;
71
+ /** Overgenomen uit de kind-definitie, niet uit de call — zie `MemoryKindDefinition.visibility`. */
72
+ visibility: MemoryVisibility;
73
+ /**
74
+ * Zichtbaarheids-ANKER (inbox-id / OwnerScope-sleutel), **geen ledenlijst**: een
75
+ * gesnapshotte ledenlijst veroudert bij elke inbox-join en reassignment, en
76
+ * stale-permissive is een lek.
77
+ */
78
+ audienceRef?: string;
79
+ /**
80
+ * Taal van de inhoud. **Beschrijvend, geen filter** — en dat is een besluit, geen omissie.
81
+ *
82
+ * Retrieval vergelijkt bewust óók over taalgrenzen: de embeddingmodellen hier zijn meertalig,
83
+ * dus een Nederlandse vraag hoort een Engelse casus over hetzelfde probleem te vinden. Er
84
+ * stond eerder een harde taalcheck in `rank.ts`; die was nooit aangesloten en zou, als je hem
85
+ * wél had aangesloten, bij een anderstalige zoekvraag élk item hebben overgeslagen — want
86
+ * niets zet dit veld, dus alles staat op de default.
87
+ *
88
+ * Zet je dit ooit echt, houd het dan een *ordenings*-signaal (materiaal in je eigen taal is
89
+ * makkelijker te gebruiken) en geen zichtbaarheidsfilter.
90
+ */
91
+ locale: string;
92
+ /** Gedenormaliseerde termen: het enige selectieve structurele filter (er is geen substring-operator). */
93
+ keywords?: string[];
94
+ /** Snelle alternatieve sleutels, bv. `tel:+31612345678` voor het rinkelmoment. */
95
+ tags?: string[];
96
+ links?: MemoryLink[];
97
+ /** Model waarmee de vector is gemaakt; retrieval vergelijkt alleen binnen hetzelfde model. */
98
+ embeddingModel?: string;
99
+ /** Uit een live pull (fase 3); nooit gepersisteerd. */
100
+ live?: boolean;
101
+ createdAt?: number;
102
+ updatedAt?: number;
103
+ }
104
+ /**
105
+ * `"audience"` = alleen voor wie het subject mag zien. `"org"` = org-breed leesbaar en
106
+ * daarmee vindbaar in het cross-subject-pad ("is dit eerder bij een andere klant
107
+ * voorgekomen?").
108
+ *
109
+ * Per **kind** gedeclareerd en niet per item: het cross-subject-pad kan zich geen
110
+ * autorisatie-round-trip per hit veroorloven, dus het filtert structureel op deze kolom.
111
+ */
112
+ export type MemoryVisibility = "audience" | "org";
@@ -0,0 +1,66 @@
1
+ import { LocaleBundle } from '../analytics/dashboard';
2
+ import { OwnerScope } from '../contact/types';
3
+ import { MemoryKindId, MemoryVisibility } from './item';
4
+ /**
5
+ * Wat er onthouden mág worden, en met welke regels.
6
+ *
7
+ * Twee bronnen, één type:
8
+ * - **app-gedeclareerd** via `GET /provider/memory/describe` (de ingebouwde defaults);
9
+ * - **org-eigen** als rij in `memory_kind`. Mal: `CustomFieldDef` — de organisatie
10
+ * definieert de sleutel, een generieke store bewaart de waarde.
11
+ *
12
+ * `ownerScope` is wat "iedereen wil zijn eigen relevante data" mogelijk maakt: sales legt
13
+ * andere dingen vast dan support, en een persoonlijk kind is alleen van die gebruiker.
14
+ */
15
+ export interface MemoryKindDefinition {
16
+ /** Org-eigen: `[a-z0-9_]+` (geen punt — die leest als een genest form-pad). App-eigen: `<app>.<naam>`. */
17
+ id: MemoryKindId;
18
+ label: string;
19
+ description?: string;
20
+ /** Subject-soorten waarop dit kind mag landen, bv. `["interaction"]` of `["contact"]`. */
21
+ subjectKinds: string[];
22
+ visibility: MemoryVisibility;
23
+ /** `"single"` = één item per (subject, kind), doorvouwen. `"many"` = losse items. */
24
+ cardinality: "single" | "many";
25
+ /** Ordening onder het tekenbudget van de bundel. */
26
+ priority: number;
27
+ /** Mag dit kind in het cross-subject-pad meedoen? Alleen zinvol bij `visibility: "org"`. */
28
+ crossSubjectSearchable: boolean;
29
+ /** Embedden kost geld; per kind aan/uit. */
30
+ embed: boolean;
31
+ retentionDays?: number;
32
+ /** Rang-multiplier: een opgeloste casus verslaat een losse notitie. Default 1. */
33
+ weight?: number;
34
+ /**
35
+ * Wie dit kind bezit. Bepaalt zowel wie mag schrijven als de **ordening** voor een
36
+ * lezer (eigen team eerst). Afwezig = de hele organisatie.
37
+ */
38
+ ownerScope?: OwnerScope;
39
+ /** Wie hem declareerde. De org kan een app-kind niet oprekken, alleen uitzetten. */
40
+ origin: "app" | "org";
41
+ enabled: boolean;
42
+ }
43
+ /**
44
+ * Bare payload van `GET /provider/memory/describe` — **niet** in `ResponseFactory`
45
+ * verpakt (mal: `AnalyticsSourceDescription`).
46
+ */
47
+ export interface MemorySourceDescription {
48
+ /** De declarerende app (== `manifest.name` == `req.source.app`). */
49
+ source: string;
50
+ kinds: MemoryKindDefinition[];
51
+ /** Vlakke LIJST: keys met een punt worden gemangeld. */
52
+ locales?: LocaleBundle;
53
+ /** Fase 3: welke subjects/kinds deze bron live kan leveren. */
54
+ live?: {
55
+ subjectKinds: string[];
56
+ kinds: MemoryKindId[];
57
+ };
58
+ }
59
+ /** De samengestelde catalogus die de hub aan de frontend levert (`GET /kinds`). */
60
+ export interface MemoryKindCatalog {
61
+ /** App-gedeclareerd + org-eigen, gemengd. */
62
+ kinds: MemoryKindDefinition[];
63
+ /** Bronnen die meededen aan de fan-out (voor "wie levert dit?"). */
64
+ sources: string[];
65
+ locales: LocaleBundle;
66
+ }
@@ -0,0 +1,113 @@
1
+ import { MemoryItem, MemoryKindId, MemorySubjectKey } from './item';
2
+ /** Grenzen van het tekenbudget van een bundel. Buiten bereik wordt geklemd, niet geweigerd. */
3
+ export declare const MIN_MEMORY_BUDGET = 500;
4
+ export declare const MAX_MEMORY_BUDGET = 12000;
5
+ export declare const DEFAULT_MEMORY_BUDGET = 4000;
6
+ /**
7
+ * Één query-contract voor het dossier-paneel, de brief tijdens het rinkelen en elke
8
+ * AI-tool. Geen tweede codepad: het verschil tussen die drie is `budget`, niet code.
9
+ */
10
+ export interface MemoryQuery {
11
+ /** Weglaten = cross-subject zoeken ("vergelijkbare cases"). */
12
+ subject?: MemorySubjectKey;
13
+ /** Extra subjects die bij hetzelfde beeld horen, bv. het contact naast de interactie. */
14
+ alsoSubjects?: MemorySubjectKey[];
15
+ /**
16
+ * Ook items die niet ópt subject staan maar er wél bij horen: alles wat dezelfde
17
+ * dossiersleutel draagt (`MemoryItem.keys`).
18
+ *
19
+ * Dit is het antwoord op "wat speelde er bij dit bedrijf": een casus staat op het gesprek,
20
+ * en het bedrijfsdossier vindt hem via `domain:`/`company:` in plaats van via een lijst van
21
+ * vijftig medewerker-subjects. Eén extra `$or`-tak op een geïndexeerde array-kolom, geen
22
+ * tweede query en geen extra autorisatie-fan-out.
23
+ *
24
+ * **Alleen `visibility: "org"` erft mee.** De autorisatie van dit pad is de ene check op het
25
+ * anker; een geërfd item is per definitie niet op dat anker geautoriseerd. Een casus uit
26
+ * iemands persoonlijke mailbox is bij het schrijven al versmald naar `audience`
27
+ * (`effectiveVisibility`) en blijft dus buiten het klantdossier van een collega.
28
+ *
29
+ * Vereist een anker (`subject`); zonder anker is dit het cross-subject-pad.
30
+ */
31
+ related?: boolean;
32
+ /** Default true. */
33
+ resolveAliases?: boolean;
34
+ kinds?: MemoryKindId[];
35
+ tags?: string[];
36
+ /** Vrije tekst. Afwezig = puur structurele ordening (priority x recency x weight) en dus geen embedding-call. */
37
+ text?: string;
38
+ since?: number;
39
+ limit?: number;
40
+ /** Zo bedoel je "bij ANDERE klanten". */
41
+ excludeSubjects?: MemorySubjectKey[];
42
+ format?: "items" | "bundle" | "both";
43
+ /** Geklemd op [{@link MIN_MEMORY_BUDGET}, {@link MAX_MEMORY_BUDGET}]. */
44
+ budget?: number;
45
+ /** Fase 3: ook live bronnen bevragen, met harde timeout en eigen sub-budget. */
46
+ live?: boolean;
47
+ }
48
+ /**
49
+ * Subject-soorten waarvoor "het dossier" de **verbanden** insluit: alles wat dezelfde
50
+ * dossiersleutel draagt, ook als het op een ander subject is onthouden.
51
+ *
52
+ * Bij een klant of een persoon ís de partij de vraag — "wat speelde er bij Van Dijck" gaat over
53
+ * het bedrijf, terwijl de casussen op de gesprekken staan. Bij een gesprek niet: dan wil je de
54
+ * draad zelf, want het hele klantverleden verdringt in een begrensde bundel precies wat er nú
55
+ * aan de hand is.
56
+ *
57
+ * Staat hier en niet in de tool of in het paneel, omdat het er twee zijn: de assistent en het
58
+ * dossierpaneel horen hetzelfde te bedoelen met "het dossier van deze klant". Twee kopieën van
59
+ * die regel is precies de soort regel die uit elkaar loopt.
60
+ */
61
+ export declare const RELATED_SUBJECT_KINDS: readonly ["contact", "company"];
62
+ /** Hoort {@link MemoryQuery.related} standaard aan te staan voor dit subject? */
63
+ export declare function relatedByDefault(subject: MemorySubjectKey | undefined): boolean;
64
+ export interface MemoryHit {
65
+ item: MemoryItem;
66
+ score: number;
67
+ snippet: string;
68
+ /** Ruw materiaal voor een confidence-gate (fase 4): waar kwam de score vandaan? */
69
+ matched: {
70
+ keyword: number;
71
+ semantic: number;
72
+ recency: number;
73
+ };
74
+ }
75
+ export interface MemoryQueryResult {
76
+ hits: MemoryHit[];
77
+ bundle?: string;
78
+ /** Hoeveel rijen het structurele voorfilter opleverde. */
79
+ scanned: number;
80
+ /** Scan-cap geraakt OF hits uit de bundel gevallen — nooit stil afkappen. */
81
+ truncated?: boolean;
82
+ /** `false` = keyword-only (geen embedding-account, of geen enkele vergelijkbare vector). */
83
+ semantic: boolean;
84
+ /** Hits die de her-autorisatie van de top-K niet overleefden. */
85
+ withheld?: number;
86
+ /**
87
+ * Kandidaten die op het cross-subject-pad afvielen op de absolute gelijkenisdrempel.
88
+ *
89
+ * Het verschil met een leeg resultaat: `belowThreshold: 7` betekent "er was materiaal, maar
90
+ * niets leek er echt op" en `undefined` betekent "er was niets". Zonder deze teller is de
91
+ * drempel niet te kalibreren — je ziet dan alleen dat er niets terugkomt, niet of hij te hoog
92
+ * staat.
93
+ */
94
+ belowThreshold?: number;
95
+ /**
96
+ * Kandidaten overgeslagen door model- of taal-mismatch. Zonder deze teller degradeert
97
+ * een modelwissel volkomen stil: `cosineSimilarity` geeft bij dimensie-mismatch 0, dus
98
+ * na het omzetten van `isDefaultEmbedding` scoort élk bestaand item 0 en valt retrieval
99
+ * geruisloos terug op keyword.
100
+ */
101
+ vectorSkipped?: number;
102
+ /**
103
+ * Waarom er niets terugkwam, als dat een structurele reden heeft in plaats van "niets
104
+ * gevonden".
105
+ *
106
+ * `"no-cross-subject-kinds"` is de belangrijkste: zoeken bij ándere klanten leest alleen
107
+ * geheugensoorten die als `visibility: "org"` én `crossSubjectSearchable` zijn
108
+ * gedeclareerd. Is er geen enkele, dan is een leeg resultaat geen zoekuitkomst maar een
109
+ * configuratiefeit — en dat hoort de caller te weten in plaats van te concluderen dat er
110
+ * niets vergelijkbaars bestaat.
111
+ */
112
+ reason?: "no-cross-subject-kinds" | "no-subject-access";
113
+ }
@@ -36,6 +36,13 @@ export interface Organization {
36
36
  billing?: OrganizationBilling;
37
37
  /** Small company logo stored inline as a base64 data-URI. */
38
38
  logo?: string;
39
+ /**
40
+ * ISO 3166-1 alpha-2 region used to turn a national phone number into E.164
41
+ * (`088 211 2121` → `+31882112121`). Absent falls back to `DEFAULT_PHONE_REGION`.
42
+ * Belongs on the organisation and not in code: hardcoding `+31` is wrong for an
43
+ * org with Belgian numbers. See `toE164` in `text/endpoint.ts`.
44
+ */
45
+ defaultPhoneRegion?: string;
39
46
  }
40
47
  /**
41
48
  * Recursive tenant hierarchy node returned by `system.tenants/tree`.
@@ -40,6 +40,18 @@ export declare const TRIGGER_VARS: readonly [{
40
40
  }, {
41
41
  readonly name: "textRaw";
42
42
  readonly type: "string";
43
+ }, {
44
+ readonly name: "fromStatus";
45
+ readonly type: "string";
46
+ }, {
47
+ readonly name: "toStatus";
48
+ readonly type: "string";
49
+ }, {
50
+ readonly name: "contactId";
51
+ readonly type: "string";
52
+ }, {
53
+ readonly name: "companyId";
54
+ readonly type: "string";
43
55
  }];
44
56
  /** De variabelen die een classify-stap oplevert, per mode. */
45
57
  export declare const CLASSIFY_VARS: {
@@ -14,6 +14,19 @@ export type PlaybookTrigger = {
14
14
  /** Specific channel ids (e.g. per mailbox). Matched against activity.channelId. */
15
15
  channelIds?: string[];
16
16
  filter?: unknown;
17
+ /**
18
+ * Wacht dit aantal milliseconden en draai dan **één** run voor de berichten in dat
19
+ * venster, in plaats van een run per bericht.
20
+ *
21
+ * Voor een playbook dat bij elk bericht een modelcall doet is dit het verschil tussen
22
+ * vier calls en één bij een mailwisseling van vier berichten in tien minuten. De run
23
+ * draait op het **nieuwste** bericht uit het venster; wat ertussen zat is in de
24
+ * samenvatting van dat gesprek al meegenomen via de lookup-stap.
25
+ *
26
+ * Bewust op de trigger en niet in de geheugenlaag: coalesceren is een eigenschap van
27
+ * "hoe vaak wil ik hierop reageren", niet van wat je onthoudt.
28
+ */
29
+ debounceMs?: number;
17
30
  } | {
18
31
  kind: "manual";
19
32
  };
@@ -59,6 +72,15 @@ export interface ClassifyStep {
59
72
  question?: string;
60
73
  fields?: unknown;
61
74
  instruction?: string;
75
+ /**
76
+ * Wélke tekst geclassificeerd wordt, als `$ref` in de run-vars. Default `$trigger.text`.
77
+ *
78
+ * Nodig zodra de trigger zelf geen tekst draagt. Een `INTERACTION_STATUS_CHANGED`-activity
79
+ * heeft alleen `{ fromStatus, toStatus }` in zijn payload, dus `trigger.text` is leeg — en
80
+ * dan krijgt het model een lege invoer. Met `input: "$thread"` leest de stap wat een
81
+ * eerdere `lookup` heeft opgehaald, zoals de berichten van het gesprek.
82
+ */
83
+ input?: string;
62
84
  }
63
85
  export interface GenerateStep {
64
86
  type: "generate";