@opencxh/domain 1.221.0 → 1.224.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.
@@ -99,6 +99,23 @@ export declare function channelKindOf(type: string): ActivityChannelKind | undef
99
99
  export declare function isMessageShape(shape: ActivityShape | undefined): boolean;
100
100
  /** See {@link isMessageShape}. For the built-in types. */
101
101
  export declare function isMessageType(type: string): boolean;
102
+ /**
103
+ * Does an answer of this kind fold into the list under the message it answers, instead of
104
+ * taking its own place in the timeline?
105
+ *
106
+ * Only notes. A mail carrying a `parentId` is still a message that was really sent, at a real
107
+ * moment, and burying it under the thing it answered would take it out of the chronology the
108
+ * rest of the conversation is read in — and out of view entirely while the thread is folded.
109
+ * An internal note has no such life of its own: it exists *about* that message.
110
+ *
111
+ * Keyed on the shape, so the rule has exactly one definition and an app-declared note kind
112
+ * behaves the same — see {@link ActivityTypeRegistry.nestsUnderParent}. `parentId` itself stays
113
+ * set on every reply regardless: it is a true fact about the row, and mail threading, the
114
+ * provider `In-Reply-To` and the reply notification all read it.
115
+ */
116
+ export declare function isNoteShape(shape: ActivityShape | undefined): boolean;
117
+ /** See {@link isNoteShape}. For the built-in types. */
118
+ export declare function nestsUnderParent(type: string): boolean;
102
119
  /** Can a human reply to it? Not to notes — those go nowhere. */
103
120
  export declare function isReplyableType(type: string): boolean;
104
121
  /**
@@ -70,6 +70,8 @@ export declare class ActivityTypeRegistry {
70
70
  * {@link ResolvedActivityType}.
71
71
  */
72
72
  isMessage(type: string): boolean;
73
+ /** Folds into the list under the message it answers. See {@link isNoteShape}. */
74
+ nestsUnderParent(type: string): boolean;
73
75
  /** Can a person reply to this? Notes cannot — those go nowhere. */
74
76
  isReplyable(type: string): boolean;
75
77
  /** Readable content for a model: messages, notes and transcripts. */
@@ -1,6 +1,7 @@
1
1
  import { ActingIdentity } from '../../platform/identity';
2
2
  import { TranscriptSegment } from '../../platform/media';
3
3
  import { AIMessageInput, AIMessageOutput } from '../ai-message/types';
4
+ import { ReactionSummary } from '../reaction/types';
4
5
  export type CallStatus = "new" | "connecting" | "ringing" | "connected" | "held" | "ended" | "failed";
5
6
  export type CallDirection = "inbound" | "outbound";
6
7
  export type CallType = "audio" | "video" | "data" | "screen-share";
@@ -34,6 +35,14 @@ export interface BaseActivity {
34
35
  contactId?: string;
35
36
  };
36
37
  direction: "inbound" | "outbound" | "internal" | "none";
38
+ /**
39
+ * The activity this one answers, if any — the in-app thread, one level deep.
40
+ *
41
+ * Not `EmailPayload.threadId`: that is the provider's own conversation id and groups a whole
42
+ * mail thread. This points at one activity, and an internal note can hang off a customer's
43
+ * mail with it. A reply to a reply stores the *top* parent, so the feed never nests twice.
44
+ */
45
+ parentId?: string;
37
46
  externalIds?: string[];
38
47
  attachments?: Attachment[];
39
48
  /**
@@ -42,6 +51,14 @@ export interface BaseActivity {
42
51
  * indicator while true.
43
52
  */
44
53
  pending?: boolean;
54
+ /**
55
+ * The emoji on this message, as the list and get routes project them.
56
+ *
57
+ * Read-only and **never stored on the activity row**: reactions are their own rows, keyed
58
+ * by (resource, actor, emoji), so two people reacting at once cannot race on one column.
59
+ * The server joins them on in one bulk query per page; a writer that sets this is ignored.
60
+ */
61
+ reactions?: ReactionSummary[];
45
62
  /** Epoch ms — matches the DB model and Interaction.createdAt. */
46
63
  createdAt?: number;
47
64
  updatedAt?: number;
@@ -128,17 +128,55 @@ export interface Assignment {
128
128
  * signal it wakes on — a loop that stops only at the turn ceiling.
129
129
  * 3. **The agent is mentioned**: wakes, whatever it is waiting on. A mention is the explicit answer
130
130
  * to the question the engine cannot answer itself: *is this addressed to my agent?*
131
- * 4. **An internal note while it waits for a reply** (`waitingOn === "reply"`): wakes. If an agent
131
+ * 4. **An answer in the agent's own thread**: wakes, whatever it is waiting on. A note hanging
132
+ * under the message the agent itself wrote is as addressed to it as its name would be — more
133
+ * so, really: you had to aim at that message to write it. This is the case the mention rule
134
+ * was standing in for before threads existed, and it deliberately widens the earlier choice
135
+ * that a mention stays required while waiting on a task. Only for its *own* thread: a reply
136
+ * under a colleague's note is two people talking, and still costs no turn.
137
+ * 5. **An internal note while it waits for a reply** (`waitingOn === "reply"`): wakes. If an agent
132
138
  * explicitly asked for an answer, the next thing a colleague writes *is* that answer; forcing
133
139
  * someone to @-mention their own agent on the question it just asked is a ritual, not a signal.
134
140
  * The worry it replaces ("two colleagues conferring cost a turn each") stays covered by the
135
- * edge: `waitingOn` has to be `"reply"`, and once it waits on a task or is still busy, a mention
136
- * is again the only way in.
141
+ * edge: `waitingOn` has to be `"reply"`, and once it waits on a task or is still busy, a
142
+ * mention or its own thread is again the only way in.
137
143
  *
138
144
  * @param waitingOn What the assignment waits on. Omitted = rules 1-3 only, so a caller that does
139
145
  * not know never wakes too eagerly.
146
+ * @param parentAuthorId Who wrote the activity this one answers, when it answers one. Resolved by
147
+ * the caller: the activity carries only `parentId`, and this function stays pure.
140
148
  */
141
- export declare function wakesAssignment(activity: FeedActivity, agentId: string, waitingOn?: WaitingOn["on"]): boolean;
149
+ export declare function wakesAssignment(activity: FeedActivity, agentId: string, waitingOn?: WaitingOn["on"], parentAuthorId?: string): boolean;
150
+ /**
151
+ * Does this reaction wake an assignment?
152
+ *
153
+ * A reaction is the cheapest possible answer: the agent asks "shall I send this?" and you put
154
+ * a ✅ on it without typing a word. That is worth having, because the alternative — typing
155
+ * "ja" and @-mentioning your own agent — is the friction that makes people not answer at all.
156
+ *
157
+ * **Its own function, not a rule inside {@link wakesAssignment}.** A reaction is not a
158
+ * `FeedActivity` and never becomes one: activities are real rows on a timeline, and
159
+ * manufacturing a fake one to reuse a signature would put a lie in the feed's own vocabulary.
160
+ *
161
+ * **Three gates, all narrow.** It must be a reaction that was *put there* (taking one away
162
+ * answers nothing); it must sit on a message this agent itself wrote (that is what makes it
163
+ * addressed — a reaction on a colleague's note is between colleagues); and the reactor must
164
+ * not be the agent. `waitingOn` does not come into it: the same standing as a mention, because
165
+ * the aiming is just as explicit.
166
+ *
167
+ * Which emoji it is, deliberately does not matter here. There is no 👍-means-yes table: the
168
+ * turn is told which emoji and from whom, and reading that is what a model is for. A fixed
169
+ * set would be a configuration that differs per team and grows by commit.
170
+ */
171
+ export declare function reactionWakesAssignment(input: {
172
+ /** The author of the activity that was reacted on. Only a `user` author can be an agent. */
173
+ targetAuthorId?: string;
174
+ /** Who reacted. */
175
+ reactorId?: string;
176
+ agentId: string;
177
+ /** `true` put it there, `false` took it away. */
178
+ on: boolean;
179
+ }): boolean;
142
180
  /** The subject of an assignment as one reference, or `undefined`. */
143
181
  export declare function assignmentSubject(assignment: {
144
182
  subjectKind?: string;
@@ -0,0 +1 @@
1
+ export * from './types';
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Who reacted.
3
+ *
4
+ * Not a bare `userId`, and not `MutationAuthor`: a reaction can come from outside the
5
+ * organisation. A Teams chat is synced with the customer in it, and their 👍 has to land
6
+ * somewhere — as a `contact`, with the name the provider gave. Mirrors `BaseActivity.author`,
7
+ * which already carries that same four-way distinction.
8
+ *
9
+ * `id` is the user id, the app name, or the provider's id for a contact; absent for `system`.
10
+ */
11
+ export interface ReactionActor {
12
+ type: "user" | "contact" | "app" | "system";
13
+ id?: string;
14
+ /** Shown on hover. Kept on the row because an external reactor has no record to look up. */
15
+ name?: string;
16
+ }
17
+ /**
18
+ * One emoji, put on one resource, by one actor.
19
+ *
20
+ * Addressed by (resourceType, resourceId) — the same `ResourceRef` shape read-state and the
21
+ * attribute store use — so an activity, a work comment, a file or a knowledge article all
22
+ * reuse this without the store learning what any of them are.
23
+ *
24
+ * A row per (resource, actor, emoji), never an array on the resource: two people reacting at
25
+ * the same moment would otherwise race on one column, and the unique index makes both the
26
+ * toggle and a re-sync from a provider idempotent.
27
+ */
28
+ export interface Reaction {
29
+ id: string;
30
+ organizationId: string;
31
+ resourceType: string;
32
+ resourceId: string;
33
+ actor: ReactionActor;
34
+ /**
35
+ * The literal character, e.g. `"👍"` — never a shortcode and never a provider's name for it.
36
+ * One vocabulary means no mapping table in the core; the six legacy Teams names are
37
+ * translated at the edge, in the app that speaks to Graph.
38
+ */
39
+ emoji: string;
40
+ createdAt?: number;
41
+ }
42
+ /** One chip: an emoji, how many put it there, and who — for the hover. */
43
+ export interface ReactionSummary {
44
+ emoji: string;
45
+ count: number;
46
+ actors: ReactionActor[];
47
+ }
48
+ /** `POST /reaction/:type/:id` body. `on` absent means "flip whatever it is now". */
49
+ export interface ToggleReactionRequest {
50
+ emoji: string;
51
+ on?: boolean;
52
+ /**
53
+ * Who to record it for, instead of the caller.
54
+ *
55
+ * **Honoured for an app-to-app caller only**, and ignored outright for a user — otherwise
56
+ * anyone could put an emoji on a message in a colleague's name. It exists for provider
57
+ * sync: a reaction pulled out of Teams belongs to the person who left it there, not to the
58
+ * app that carried it across.
59
+ */
60
+ actor?: ReactionActor;
61
+ }
62
+ /** Reactions per resource id, as the list endpoints hand them to a feed. */
63
+ export type ReactionsByResource = Record<string, ReactionSummary[]>;
64
+ /** The one thing that just happened, as opposed to the state that resulted from it. */
65
+ export interface ReactionChange {
66
+ emoji: string;
67
+ actor: ReactionActor;
68
+ /** `true` put it there, `false` took it away. */
69
+ on: boolean;
70
+ }
71
+ /**
72
+ * The `reactions:changed` event, as apps receive it (`communication:reactions:changed`).
73
+ *
74
+ * Carries **both** the resulting chip set and the delta, because the two consumers need
75
+ * different things. A feed re-renders from `reactions` — a client that only got deltas and
76
+ * missed one would keep a wrong count forever. A server-side listener needs `change`: "who
77
+ * just did what" cannot be derived from a state it never saw the previous version of.
78
+ *
79
+ * `organizationId` rides along because an event handler has no request: every other
80
+ * subscriber in this platform reads the tenant from the payload and gives up without it.
81
+ */
82
+ export interface ReactionsChangedEvent {
83
+ organizationId: string;
84
+ resourceType: string;
85
+ resourceId: string;
86
+ reactions: ReactionSummary[];
87
+ change: ReactionChange;
88
+ }
89
+ /**
90
+ * Rows to chips.
91
+ *
92
+ * Most-used first so the chip that carries the most agreement sits nearest the message, with
93
+ * ties broken by which emoji appeared first — a stable order, so a chip does not jump when
94
+ * someone else reacts. Pure: this is the one piece of the feature worth a test.
95
+ */
96
+ export declare function summarizeReactions(rows: Reaction[]): ReactionSummary[];
97
+ /** Did this user put this emoji there? The chip's own state, asked without a second query. */
98
+ export declare function hasReacted(summary: ReactionSummary, userId: string | undefined): boolean;
@@ -1,3 +1,4 @@
1
+ import { ReactionSummary } from '../reaction/types';
1
2
  import { WorkStatusCategory } from './types';
2
3
  /**
3
4
  * The activity feed under a work item.
@@ -36,6 +37,8 @@ export interface WorkActivity {
36
37
  * "the customer answered" and would wake every waiting assignment on the item.
37
38
  */
38
39
  direction: "internal";
40
+ /** The activity this one answers — same one-level thread as comms' `BaseActivity.parentId`. */
41
+ parentId?: string;
39
42
  /** `html` is the composer's markup for a comment; `text` stays the plain reading of it. */
40
43
  payload: {
41
44
  text?: string;
@@ -46,6 +49,12 @@ export interface WorkActivity {
46
49
  toStatus?: string;
47
50
  toCategory?: WorkStatusCategory;
48
51
  };
52
+ /**
53
+ * The emoji on this comment, as the list route projects them. Read-only and never stored
54
+ * on the row — see `BaseActivity.reactions`, which this mirrors, and the generic store in
55
+ * the communication app that both read from.
56
+ */
57
+ reactions?: ReactionSummary[];
49
58
  createdAt?: number;
50
59
  updatedAt?: number;
51
60
  }
@@ -10,8 +10,17 @@ import { WorkItem, WorkProject } from './types';
10
10
  /** The scope kinds `apps/work` claims through `/provider/scope/describe`. */
11
11
  export declare const WORK_ITEM_SCOPE_KIND = "work_item";
12
12
  export declare const WORK_PROJECT_SCOPE_KIND = "work_project";
13
+ /**
14
+ * A row in an item's feed, addressable on its own.
15
+ *
16
+ * Claimed so the generic stores can hang something off a single comment — reactions today,
17
+ * whatever is resource-agnostic tomorrow. It inherits its item's access exactly the way
18
+ * comms' `activity:` inherits its conversation's: a comment is not a second boundary.
19
+ */
20
+ export declare const WORK_ACTIVITY_SCOPE_KIND = "work_activity";
13
21
  export declare function workItemScopeKey(itemId: string): string;
14
22
  export declare function workProjectScopeKey(projectId: string): string;
23
+ export declare function workActivityScopeKey(activityId: string): string;
15
24
  /**
16
25
  * The human key: `SAL-142`, or `SAL-142-1` for a subitem.
17
26
  *