@company-semantics/contracts 58.1.0 → 58.3.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/package.json +1 -1
  2. package/src/__tests__/resource-keys.test.ts +30 -0
  3. package/src/api/generated-spec-hash.ts +2 -2
  4. package/src/api/generated.ts +77 -1
  5. package/src/chat/README.md +15 -4
  6. package/src/chat/__tests__/proactive-kind.test.ts +51 -0
  7. package/src/chat/index.ts +9 -0
  8. package/src/chat/proactive-kind.ts +51 -0
  9. package/src/chat/schemas.ts +92 -1
  10. package/src/chat/types.ts +19 -1
  11. package/src/generated/openapi-routes.ts +1 -0
  12. package/src/index.ts +58 -0
  13. package/src/message-parts/README.md +5 -0
  14. package/src/message-parts/__tests__/suggested-replies.test.ts +52 -0
  15. package/src/message-parts/__tests__/wire.test.ts +48 -0
  16. package/src/message-parts/index.ts +8 -0
  17. package/src/message-parts/suggested-replies.ts +48 -0
  18. package/src/message-parts/types.ts +7 -1
  19. package/src/message-parts/wire.ts +26 -0
  20. package/src/proactive/README.md +125 -0
  21. package/src/proactive/__tests__/README.md +56 -0
  22. package/src/proactive/__tests__/chat-templates.test.ts +167 -0
  23. package/src/proactive/__tests__/compile-fixtures.ts +110 -0
  24. package/src/proactive/__tests__/vocabulary.test.ts +279 -0
  25. package/src/proactive/classes.ts +125 -0
  26. package/src/proactive/composer.ts +104 -0
  27. package/src/proactive/facts.ts +87 -0
  28. package/src/proactive/index.ts +52 -0
  29. package/src/proactive/kinds.ts +127 -0
  30. package/src/proactive/plan.ts +79 -0
  31. package/src/proactive/registry.ts +71 -0
  32. package/src/proactive/surfaces.ts +59 -0
  33. package/src/proactive/templates/README.md +58 -0
  34. package/src/proactive/templates/index.ts +32 -0
  35. package/src/proactive/templates/morning-brief.ts +77 -0
  36. package/src/proactive/templates/org-became-shared.ts +54 -0
  37. package/src/resource-key-types.ts +9 -0
  38. package/src/resource-keys.ts +2 -0
  39. package/src/user-notifications/README.md +10 -0
  40. package/src/user-notifications/kinds.ts +28 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@company-semantics/contracts",
3
- "version": "58.1.0",
3
+ "version": "58.3.0",
4
4
  "private": false,
5
5
  "repository": {
6
6
  "type": "git",
@@ -80,6 +80,36 @@ describe("resource-keys: directGrants (org-scoped)", () => {
80
80
  });
81
81
  });
82
82
 
83
+ describe("resource-keys: orgSystemEvents (org-scoped)", () => {
84
+ // The banner strip's invalidation key (PRD-00958). Registered in contracts
85
+ // rather than the app's AppOnlyKey union because the SERVER publishes it over
86
+ // the user-event stream — and a key the registry does not know cannot be
87
+ // routed through the SSE coalescer at all.
88
+ const key: ResourceKey = { type: "orgSystemEvents", orgId: ORG_ID };
89
+
90
+ it("toQueryKey produces [type, orgId]", () => {
91
+ const qk = toQueryKey(key);
92
+ expect(qk).toEqual(["orgSystemEvents", ORG_ID]);
93
+ expect(qk).toHaveLength(2);
94
+ });
95
+
96
+ it("roundtrips through fromQueryKey", () => {
97
+ expect(fromQueryKey(toQueryKey(key))).toEqual(key);
98
+ });
99
+
100
+ it("is recognised by isResourceKeyShape", () => {
101
+ // The wire-boundary predicate that gates `resource.invalidated` frames; a
102
+ // key it rejects never reaches the coalescer, so the banner never refreshes.
103
+ expect(isResourceKeyShape(key)).toBe(true);
104
+ });
105
+
106
+ it("matchesResourceKey is org-scoped: rejects a different orgId", () => {
107
+ const other = "22222222-2222-4222-8222-222222222222";
108
+ expect(matchesResourceKey(toQueryKey(key), key)).toBe(true);
109
+ expect(matchesResourceKey(["orgSystemEvents", other], key)).toBe(false);
110
+ });
111
+ });
112
+
83
113
  describe("resource-keys: orgUnitMyAuthority (per-unit identity)", () => {
84
114
  const UNIT_ID = "33333333-3333-4333-8333-333333333333";
85
115
  const key: ResourceKey = {
@@ -1,3 +1,3 @@
1
1
  // AUTO-GENERATED — do not edit. Run pnpm generate:spec-hash to regenerate.
2
- export const SPEC_HASH = '49b741e4372d' as const;
3
- export const SPEC_HASH_FULL = '49b741e4372d56fb1a87312fe62f6e86ffd4987de590d03414ee833a4b6586b0' as const;
2
+ export const SPEC_HASH = '433bb009d9f1' as const;
3
+ export const SPEC_HASH_FULL = '433bb009d9f1037871adde049808503dc040cc4cb44122a8f2d603da02b4dd2b' as const;
@@ -465,6 +465,26 @@ export interface paths {
465
465
  patch?: never;
466
466
  trace?: never;
467
467
  };
468
+ "/api/chats/proactive/{eventId}": {
469
+ parameters: {
470
+ query?: never;
471
+ header?: never;
472
+ path?: never;
473
+ cookie?: never;
474
+ };
475
+ /**
476
+ * Resolve a proactive occurrence to the current reader's chat
477
+ * @description A banner or inbox row links to an OCCURRENCE (/chat/proactive/{eventId}), never to a chat id, because the pushed chat is per recipient. This resolves the occurrence for the CURRENT reader: `ready` with their chat id, `pending` while the projection job is still running or retrying, `unavailable` when it gave up (the durable inbox row stands alone), or `ineligible` when the reader is not in the occurrence’s frozen audience.
478
+ */
479
+ get: operations["resolveProactiveChat"];
480
+ put?: never;
481
+ post?: never;
482
+ delete?: never;
483
+ options?: never;
484
+ head?: never;
485
+ patch?: never;
486
+ trace?: never;
487
+ };
468
488
  "/api/workspace/access": {
469
489
  parameters: {
470
490
  query?: never;
@@ -3994,7 +4014,12 @@ export interface components {
3994
4014
  pinnedAt: string | null;
3995
4015
  titleSource: ("auto" | "manual") | null;
3996
4016
  titleGeneratedAt: string | null;
4017
+ /** @enum {string} */
4018
+ origin?: "user" | "proactive";
4019
+ unread?: boolean;
4020
+ proactiveKind?: string | null;
3997
4021
  }[];
4022
+ unreadCount?: number;
3998
4023
  };
3999
4024
  CreateChatResponse: {
4000
4025
  chat: {
@@ -4009,6 +4034,10 @@ export interface components {
4009
4034
  pinnedAt: string | null;
4010
4035
  titleSource: ("auto" | "manual") | null;
4011
4036
  titleGeneratedAt: string | null;
4037
+ /** @enum {string} */
4038
+ origin?: "user" | "proactive";
4039
+ unread?: boolean;
4040
+ proactiveKind?: string | null;
4012
4041
  };
4013
4042
  };
4014
4043
  CreateChatRequest: {
@@ -4036,6 +4065,9 @@ export interface components {
4036
4065
  createdAt: string;
4037
4066
  /** Format: date-time */
4038
4067
  updatedAt: string;
4068
+ /** @enum {string} */
4069
+ origin?: "user" | "proactive";
4070
+ unread?: boolean;
4039
4071
  };
4040
4072
  messages: {
4041
4073
  id: string;
@@ -4166,6 +4198,10 @@ export interface components {
4166
4198
  /** Format: date-time */
4167
4199
  updatedAt: string;
4168
4200
  isShared: boolean;
4201
+ /** @enum {string} */
4202
+ origin?: "user" | "proactive";
4203
+ unread?: boolean;
4204
+ proactiveKind?: string | null;
4169
4205
  };
4170
4206
  } | {
4171
4207
  /** @constant */
@@ -4187,6 +4223,10 @@ export interface components {
4187
4223
  /** Format: date-time */
4188
4224
  updatedAt: string;
4189
4225
  isShared: boolean;
4226
+ /** @enum {string} */
4227
+ origin?: "user" | "proactive";
4228
+ unread?: boolean;
4229
+ proactiveKind?: string | null;
4190
4230
  };
4191
4231
  changed: ("title" | "titleSource" | "pinnedAt")[];
4192
4232
  } | {
@@ -4227,6 +4267,20 @@ export interface components {
4227
4267
  id?: string;
4228
4268
  runtimeProfile?: string;
4229
4269
  };
4270
+ ProactiveChatResolution: {
4271
+ /** @constant */
4272
+ state: "ready";
4273
+ chatId: string;
4274
+ } | {
4275
+ /** @constant */
4276
+ state: "pending";
4277
+ } | {
4278
+ /** @constant */
4279
+ state: "unavailable";
4280
+ } | {
4281
+ /** @constant */
4282
+ state: "ineligible";
4283
+ };
4230
4284
  WorkspaceAccessResponse: {
4231
4285
  hasAccess: boolean;
4232
4286
  };
@@ -6232,7 +6286,7 @@ export interface components {
6232
6286
  notification: {
6233
6287
  id: string;
6234
6288
  /** @enum {string} */
6235
- kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply";
6289
+ kind: "companyMd.access_requested" | "companyMd.access_request_approved" | "companyMd.access_request_denied" | "comment.mention" | "comment.reply" | "proactive.org_became_shared" | "proactive.brief_morning";
6236
6290
  target: {
6237
6291
  /** @enum {string} */
6238
6292
  type: "company_md" | "org_unit" | "org" | "execution";
@@ -7816,6 +7870,28 @@ export interface operations {
7816
7870
  };
7817
7871
  };
7818
7872
  };
7873
+ resolveProactiveChat: {
7874
+ parameters: {
7875
+ query?: never;
7876
+ header?: never;
7877
+ path: {
7878
+ eventId: string;
7879
+ };
7880
+ cookie?: never;
7881
+ };
7882
+ requestBody?: never;
7883
+ responses: {
7884
+ /** @description The resolution for the current reader (one of four bounded states) */
7885
+ 200: {
7886
+ headers: {
7887
+ [name: string]: unknown;
7888
+ };
7889
+ content: {
7890
+ "application/json": components["schemas"]["ProactiveChatResolution"];
7891
+ };
7892
+ };
7893
+ };
7894
+ };
7819
7895
  getWorkspaceAccess: {
7820
7896
  parameters: {
7821
7897
  query?: never;
@@ -12,6 +12,11 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
12
12
  - Invalidation events ensure convergence after SSE disconnects
13
13
  - Runtime profile labels are vendor-agnostic (no model names in UI)
14
14
  - Default runtime profile is `agentic`
15
+ - The proactive wire fields (`origin`, `unread`, `proactiveKind`) ride the extended summary AND both chat SSE event payloads — the app builds a sidebar row from event data alone, so a field missing from an event schema means every pushed chat arrives silently read (PRD-00958, ADR-CONTRACTS-142)
16
+ - `unread` is a ONE-TIME proactive badge, cleared permanently on open; ordinary conversation never sets it again
17
+ - `proactiveKind` is a bare string on the wire so a newly shipped kind cannot make an older client reject the whole chat list; it becomes a `ProactiveEventKind` exactly once, at the edge, via `recognizeProactiveKind`
18
+ - `unreadCount` on the GET /api/chats envelope is the total across all the reader's chats, never derived from the returned page (app ADR slug `feed-mark-on-open-and-paging`)
19
+ - `ProactiveChatResolution` has four bounded states (`ready` / `pending` / `unavailable` / `ineligible`) — never an open-ended "preparing"
15
20
 
16
21
  <!-- BEGIN GENERATED: readme-public-api — derived from code by `pnpm readme-api`. Do not edit. -->
17
22
 
@@ -25,7 +30,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
25
30
  - `ChatChangedField` _(type)_ — Fields that can change on a chat, used in changed array.
26
31
  - `ChatChangedFieldSchema`
27
32
  - `ChatCreatedEvent` _(type)_ — Emitted exactly once after a chat is successfully persisted.
28
- - `ChatCreatedEventSchema`
33
+ - `ChatCreatedEventSchema` — Emitted exactly once after a chat is persisted; a pushed proactive chat is announced over this event too.
29
34
  - `ChatDeletedEvent` _(type)_ — Emitted when a chat is deleted.
30
35
  - `ChatDeletedEventSchema`
31
36
  - `ChatDetail` _(type)_ — Chat detail object in GET /api/chats/:id — includes interactionId for append-on-edit flows.
@@ -36,9 +41,11 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
36
41
  - `ChatInvalidationEventSchema`
37
42
  - `ChatListFilters` _(type)_ — Filters for listing chats.
38
43
  - `ChatListResponse` _(type)_ — Response for GET /api/chats
39
- - `ChatListResponseSchema` — Response for GET /api/chats
44
+ - `ChatListResponseSchema` — Response for GET /api/chats.
40
45
  - `ChatMessage` _(type)_ — Individual message in a chat response (GET /api/chats/:id).
41
46
  - `ChatMessageSchema` — Individual message in a chat, as returned by GET /api/chats/:id.
47
+ - `ChatOrigin` _(type)_ — How a chat was born: `user` or `proactive` (the system spoke first).
48
+ - `ChatOriginSchema` — How a chat was born: by the user, or by the system speaking first (a proactive occurrence projected onto the…
42
49
  - `ChatRuntimeProfile` _(type)_ — Chat runtime profile — user-selectable orchestration strategy.
43
50
  - `ChatRuntimeProfileInfo` _(type)_ — Runtime profile metadata for UI rendering.
44
51
  - `ChatShareInfo` _(type)_ — Chat share record with snapshot metadata.
@@ -48,7 +55,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
48
55
  - `ChatSuccessResponse` _(type)_ — Generic mutation success confirmation.
49
56
  - `ChatSuccessResponseSchema` — Generic success confirmation for mutations that return no entity data.
50
57
  - `ChatSummary` _(type)_ — Lightweight chat summary for list views.
51
- - `ChatSummaryExtended` _(type)_ — Extended chat summary with pin/title status.
58
+ - `ChatSummaryExtended` _(type)_ — Extended chat summary with pin/title status, plus the optional proactive wire fields (`origin`, `unread`…
52
59
  - `ChatSummaryExtendedSchema`
53
60
  - `ChatSummarySchema`
54
61
  - `ChatUpdatedEvent` _(type)_ — Emitted when chat metadata changes (title, pin, etc.) Carries full current state - clients should replace…
@@ -72,6 +79,9 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
72
79
  - `IsoDateString` — ISO 8601 datetime string — the runtime safety net for date serialization.
73
80
  - `ListSharesResponse` _(type)_ — Response for GET /api/chats/:chatId/shares
74
81
  - `ListSharesResponseSchema` — Response for GET /api/chats/:chatId/shares
82
+ - `ProactiveChatResolution` _(type)_ — Resolving an occurrence id to the CURRENT reader's chat — one of the four bounded states `ready` (with…
83
+ - `ProactiveChatResolutionSchema` — Resolving an occurrence id to THIS reader's chat (INV-PROACTIVE-LINK).
84
+ - `RecognizedProactiveKind` _(type)_ — A wire `proactiveKind` after recognition: a kind this contracts version knows, or the raw string it did not —…
75
85
  - `SharedChatMessage` _(type)_ — Individual message in a shared chat view.
76
86
  - `SharedChatMessageSchema`
77
87
  - `SharedChatView` _(type)_ — Shared chat view returned to viewers.
@@ -84,6 +94,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
84
94
  - `UpdateShareRequest` _(type)_ — Request to update an existing share.
85
95
  - `UpdateShareResponse` _(type)_ — Response for PATCH /api/shares/:shareId
86
96
  - `UpdateShareResponseSchema` — Response for PATCH /api/shares/:shareId
97
+ - `recognizeProactiveKind` — Recognize a wire `proactiveKind`.
87
98
 
88
99
  <!-- END GENERATED: readme-public-api -->
89
100
 
@@ -93,7 +104,7 @@ Shared types for chat persistence, sharing, real-time events, and runtime profil
93
104
 
94
105
  **Internal domains:**
95
106
 
96
- _None._
107
+ - `proactive`
97
108
 
98
109
  **External packages:**
99
110
 
@@ -0,0 +1,51 @@
1
+ /**
2
+ * recognizeProactiveKind — the ONE conversion from the wire-lenient
3
+ * `proactiveKind` string to a ProactiveEventKind (PRD-00958).
4
+ *
5
+ * The wire schema accepts any string so a new kind cannot make an older
6
+ * client reject the whole chat list; these tests pin that the looseness stops
7
+ * here, and that "not proactive" and "proactive but unknown" never collapse.
8
+ */
9
+ import { describe, expect, it } from "vitest";
10
+ import { PROACTIVE_EVENT_KIND_IDS } from "../../proactive/kinds.js";
11
+ import { recognizeProactiveKind } from "../proactive-kind.js";
12
+
13
+ describe("recognizeProactiveKind", () => {
14
+ it("returns null for an absent kind (a user-born chat)", () => {
15
+ expect(recognizeProactiveKind(null)).toBeNull();
16
+ expect(recognizeProactiveKind(undefined)).toBeNull();
17
+ expect(recognizeProactiveKind("")).toBeNull();
18
+ });
19
+
20
+ it("recognizes every member of PROACTIVE_EVENT_KIND_IDS as itself", () => {
21
+ // Total over the vocabulary: a kind added to ../proactive/kinds is
22
+ // recognized here with no edit, because there is no second list to drift.
23
+ for (const kind of PROACTIVE_EVENT_KIND_IDS) {
24
+ expect(recognizeProactiveKind(kind)).toBe(kind);
25
+ }
26
+ });
27
+
28
+ it("carries an unknown kind as { unknown } rather than dropping it", () => {
29
+ // Distinct from null: a pushed chat of a kind this version has not learned
30
+ // must still render as a (generic) proactive chat, never as a user chat.
31
+ expect(recognizeProactiveKind("future.kind_not_in_this_version")).toEqual({
32
+ unknown: "future.kind_not_in_this_version",
33
+ });
34
+ });
35
+
36
+ it("does not recognize an inbox kind string as an event kind", () => {
37
+ // The inbox kind (`proactive.org_became_shared`) is DELIBERATELY distinct
38
+ // from the event kind (`org.became_shared`); recognition must not blur
39
+ // the two unions.
40
+ expect(recognizeProactiveKind("proactive.org_became_shared")).toEqual({
41
+ unknown: "proactive.org_became_shared",
42
+ });
43
+ });
44
+
45
+ it("is pure: the same input yields the same result across calls", () => {
46
+ expect(recognizeProactiveKind("org.became_shared")).toBe(
47
+ recognizeProactiveKind("org.became_shared"),
48
+ );
49
+ expect(recognizeProactiveKind("x")).toEqual(recognizeProactiveKind("x"));
50
+ });
51
+ });
package/src/chat/index.ts CHANGED
@@ -20,9 +20,12 @@ export type {
20
20
  // Chat lifecycle types
21
21
  TitleSource,
22
22
  ChatListFilters,
23
+ ChatOrigin,
23
24
  ChatSummaryExtended,
24
25
  TitleGenerationRequest,
25
26
  TitleGenerationResponse,
27
+ // Proactive chat resolution (occurrence id → this reader's chat)
28
+ ProactiveChatResolution,
26
29
  // Event system types
27
30
  BaseEvent,
28
31
  InvalidationReason,
@@ -61,8 +64,10 @@ export {
61
64
  TitleSourceSchema,
62
65
  InvalidationReasonSchema,
63
66
  ChatChangedFieldSchema,
67
+ ChatOriginSchema,
64
68
  ChatSummarySchema,
65
69
  ChatSummaryExtendedSchema,
70
+ ProactiveChatResolutionSchema,
66
71
  TitleGenerationResponseSchema,
67
72
  SharedChatMessageSchema,
68
73
  ChatShareInfoSchema,
@@ -89,6 +94,10 @@ export {
89
94
  GetChatResponseSchema,
90
95
  } from "./schemas";
91
96
 
97
+ // Recognizing a wire `proactiveKind` — once, at the edge
98
+ export type { RecognizedProactiveKind } from "./proactive-kind";
99
+ export { recognizeProactiveKind } from "./proactive-kind";
100
+
92
101
  // Runtime profile types and constants
93
102
  export type {
94
103
  ChatRuntimeProfile,
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Recognizing a chat's `proactiveKind` — ONCE, at the edge.
3
+ *
4
+ * On the wire `proactiveKind` is a bare `z.string()` (`./schemas`), so a kind
5
+ * shipped by a newer backend cannot make an older client reject the WHOLE
6
+ * chat list. That looseness must stop here: the domain model speaks
7
+ * `ProactiveEventKind`, and this is the only place a raw string is allowed to
8
+ * become one.
9
+ *
10
+ * INVARIANTS:
11
+ * - Recognition happens ONCE, at the boundary where the parsed wire shape is
12
+ * turned into domain state. Downstream code takes the recognized value and
13
+ * must never re-derive it from the raw string, and must never treat an
14
+ * unrecognised string as a valid current-domain kind — an older client
15
+ * renders a generic proactive chat for `{ unknown }` instead.
16
+ * - `null` means "not a proactive chat" (origin `user`, or an absent field).
17
+ * It is distinct from `{ unknown }`, which means "a proactive chat of a kind
18
+ * this contracts version has not learned" — the two must not collapse, or a
19
+ * pushed chat from a newer kind silently renders as a user chat.
20
+ * - The vocabulary is `PROACTIVE_EVENT_KIND_IDS` and nothing else; there is no
21
+ * second list here to drift from `../proactive`.
22
+ *
23
+ * @see ../proactive/kinds.ts
24
+ */
25
+
26
+ import { PROACTIVE_EVENT_KIND_IDS } from "../proactive/kinds";
27
+ import type { ProactiveEventKind } from "../proactive/kinds";
28
+
29
+ /**
30
+ * A wire `proactiveKind` after recognition: a kind this contracts version
31
+ * knows, or the raw string it did not — carried, not discarded, so the
32
+ * unknown can still be logged and rendered generically.
33
+ */
34
+ export type RecognizedProactiveKind = ProactiveEventKind | { unknown: string };
35
+
36
+ /**
37
+ * Recognize a wire `proactiveKind`. Pure, total, and the ONLY sanctioned
38
+ * conversion from the wire string to `ProactiveEventKind`.
39
+ *
40
+ * - `null` / `undefined` / `""` → `null`: not a proactive chat.
41
+ * - a member of `PROACTIVE_EVENT_KIND_IDS` → that `ProactiveEventKind`.
42
+ * - anything else → `{ unknown: raw }`: proactive, of a kind not yet known.
43
+ */
44
+ export function recognizeProactiveKind(
45
+ raw: string | null | undefined,
46
+ ): RecognizedProactiveKind | null {
47
+ if (!raw) return null;
48
+ return (PROACTIVE_EVENT_KIND_IDS as readonly string[]).includes(raw)
49
+ ? (raw as ProactiveEventKind)
50
+ : { unknown: raw };
51
+ }
@@ -24,6 +24,52 @@ export const ChatChangedFieldSchema = z.enum([
24
24
  "pinnedAt",
25
25
  ]);
26
26
 
27
+ /**
28
+ * How a chat was born: by the user, or by the system speaking first (a
29
+ * proactive occurrence projected onto the chat surface — `../proactive`).
30
+ * A CLOSED set on the wire, unlike `proactiveKind`: the two origins are the
31
+ * whole space, and an older client must know which of the two it holds.
32
+ */
33
+ export const ChatOriginSchema = z.enum(["user", "proactive"]);
34
+
35
+ /**
36
+ * The three proactive wire fields, stated ONCE and spread into every shape
37
+ * that carries a chat: the extended summary, BOTH chat SSE event payloads and
38
+ * (origin + unread) the detail. The app's `eventDataToSummary()` builds a full
39
+ * `ChatSummaryExtended` out of event data — a field on the summary but not on
40
+ * the event schema means every pushed chat arrives silently READ, with no type
41
+ * error to catch it. Spreading one definition is what makes that drift a
42
+ * compile error instead of a bug report.
43
+ *
44
+ * All three are OPTIONAL in this wave so consumer object literals (the app's
45
+ * `insertPendingChat` and `eventDataToSummary`) keep compiling. Dropping
46
+ * `.optional()` is a later major release.
47
+ */
48
+ const proactiveChatFields = {
49
+ /** How the chat was born. Absent means `user`. */
50
+ origin: ChatOriginSchema.optional(),
51
+ /**
52
+ * A ONE-TIME proactive badge, not a general chat-read model.
53
+ *
54
+ * unread <=> origin = 'proactive' AND unread_at IS NOT NULL
55
+ *
56
+ * Opening the chat clears it permanently; ordinary conversation NEVER makes
57
+ * the thread unread again. Deliberately a boolean rather than a timestamp:
58
+ * there is no "new since" semantics in a one-time flag.
59
+ */
60
+ unread: z.boolean().optional(),
61
+ /**
62
+ * Which ProactiveEventKind produced it; null for `origin: "user"`.
63
+ *
64
+ * DELIBERATELY `z.string()`, not `z.enum(PROACTIVE_EVENT_KIND_IDS)` — a newly
65
+ * shipped kind must not make an older client reject the WHOLE chat list. The
66
+ * looseness stops at the wire: call `recognizeProactiveKind` (in
67
+ * `./proactive-kind`) ONCE at the edge, and never treat the raw string as a
68
+ * current-domain kind downstream.
69
+ */
70
+ proactiveKind: z.string().nullable().optional(),
71
+ };
72
+
27
73
  // =============================================================================
28
74
  // Core Entities
29
75
  // =============================================================================
@@ -41,6 +87,7 @@ export const ChatSummaryExtendedSchema = ChatSummarySchema.extend({
41
87
  pinnedAt: IsoDateString.nullable(),
42
88
  titleSource: TitleSourceSchema.nullable(),
43
89
  titleGeneratedAt: IsoDateString.nullable(),
90
+ ...proactiveChatFields,
44
91
  });
45
92
 
46
93
  export const TitleGenerationResponseSchema = z.object({
@@ -105,6 +152,14 @@ export const BaseEventSchema = z.object({
105
152
  eventId: z.string().optional(),
106
153
  });
107
154
 
155
+ /**
156
+ * Emitted exactly once after a chat is persisted; a pushed proactive chat is
157
+ * announced over this event too.
158
+ *
159
+ * The SAME three proactive fields ride both chat event payloads (see
160
+ * `proactiveChatFields`) because the app builds its sidebar row from `data`
161
+ * alone.
162
+ */
108
163
  export const ChatCreatedEventSchema = BaseEventSchema.extend({
109
164
  type: z.literal("chat.created"),
110
165
  data: z.object({
@@ -117,6 +172,7 @@ export const ChatCreatedEventSchema = BaseEventSchema.extend({
117
172
  createdAt: IsoDateString,
118
173
  updatedAt: IsoDateString,
119
174
  isShared: z.boolean(),
175
+ ...proactiveChatFields,
120
176
  }),
121
177
  });
122
178
 
@@ -132,6 +188,7 @@ export const ChatUpdatedEventSchema = BaseEventSchema.extend({
132
188
  createdAt: IsoDateString,
133
189
  updatedAt: IsoDateString,
134
190
  isShared: z.boolean(),
191
+ ...proactiveChatFields,
135
192
  }),
136
193
  changed: z.array(ChatChangedFieldSchema),
137
194
  });
@@ -205,11 +262,40 @@ export const ChatByInteractionResponseSchema = z.object({
205
262
  chatId: z.string(),
206
263
  });
207
264
 
208
- /** Response for GET /api/chats */
265
+ /**
266
+ * Response for GET /api/chats.
267
+ *
268
+ * `unreadCount` rides the ENVELOPE, never derived from the returned page. The
269
+ * sidebar list is a 50-row page; deriving a badge from it is the windowed-count
270
+ * failure the app ADR slug `feed-mark-on-open-and-paging` forbids. It is the
271
+ * total across ALL of this reader's chats, and optional only because an older
272
+ * backend does not send it (a missing count is "no badge", never "zero").
273
+ */
209
274
  export const ChatListResponseSchema = z.object({
210
275
  chats: z.array(ChatSummaryExtendedSchema),
276
+ unreadCount: z.number().int().nonnegative().optional(),
211
277
  });
212
278
 
279
+ /**
280
+ * Resolving an occurrence id to THIS reader's chat (INV-PROACTIVE-LINK). A
281
+ * banner or inbox row links to the occurrence, not to a chat id, because the
282
+ * chat is per recipient; the server resolves it for the CURRENT reader.
283
+ *
284
+ * Bounded states, never an open-ended "preparing": the first time a chat
285
+ * projection exhausts its retries while the banner already exists, an
286
+ * unbounded pending state becomes a dead link.
287
+ * - `ready`: the reader's chat exists; open it.
288
+ * - `pending`: the projection is still being written; try again shortly.
289
+ * - `unavailable`: the projection gave up; the durable inbox row stands alone.
290
+ * - `ineligible`: the reader is not in the occurrence's frozen audience.
291
+ */
292
+ export const ProactiveChatResolutionSchema = z.discriminatedUnion("state", [
293
+ z.object({ state: z.literal("ready"), chatId: z.string() }),
294
+ z.object({ state: z.literal("pending") }),
295
+ z.object({ state: z.literal("unavailable") }),
296
+ z.object({ state: z.literal("ineligible") }),
297
+ ]);
298
+
213
299
  /** Response for POST /api/chats */
214
300
  export const CreateChatResponseSchema = z.object({
215
301
  chat: ChatSummaryExtendedSchema,
@@ -244,6 +330,11 @@ export const ChatDetailSchema = z.object({
244
330
  titleGeneratedAt: IsoDateString.nullable().optional(),
245
331
  createdAt: IsoDateString,
246
332
  updatedAt: IsoDateString,
333
+ // Opening the detail is what clears `unread`; the detail carries the flag so
334
+ // the reader can tell a first open from a return visit. `proactiveKind` is
335
+ // a list concern (the sidebar row) and does not ride the detail.
336
+ origin: proactiveChatFields.origin,
337
+ unread: proactiveChatFields.unread,
247
338
  });
248
339
 
249
340
  /** Response for GET /api/chats/:id */
package/src/chat/types.ts CHANGED
@@ -12,8 +12,10 @@ import {
12
12
  TitleSourceSchema,
13
13
  InvalidationReasonSchema,
14
14
  ChatChangedFieldSchema,
15
+ ChatOriginSchema,
15
16
  ChatSummarySchema,
16
17
  ChatSummaryExtendedSchema,
18
+ ProactiveChatResolutionSchema,
17
19
  TitleGenerationResponseSchema,
18
20
  ChatShareInfoSchema,
19
21
  SharedChatViewSchema,
@@ -132,10 +134,26 @@ export interface ChatListFilters {
132
134
  }
133
135
 
134
136
  /**
135
- * Extended chat summary with pin/title status.
137
+ * How a chat was born: `user` or `proactive` (the system spoke first).
138
+ */
139
+ export type ChatOrigin = z.infer<typeof ChatOriginSchema>;
140
+
141
+ /**
142
+ * Extended chat summary with pin/title status, plus the optional proactive
143
+ * wire fields (`origin`, `unread`, `proactiveKind`). Recognize `proactiveKind`
144
+ * once at the edge with `recognizeProactiveKind` — it is a bare string here.
136
145
  */
137
146
  export type ChatSummaryExtended = z.infer<typeof ChatSummaryExtendedSchema>;
138
147
 
148
+ /**
149
+ * Resolving an occurrence id to the CURRENT reader's chat — one of the four
150
+ * bounded states `ready` (with `chatId`), `pending`, `unavailable`,
151
+ * `ineligible`. Never an open-ended "preparing".
152
+ */
153
+ export type ProactiveChatResolution = z.infer<
154
+ typeof ProactiveChatResolutionSchema
155
+ >;
156
+
139
157
  /**
140
158
  * Request to generate a title for a chat.
141
159
  */
@@ -15,6 +15,7 @@ export const openApiRoutes = {
15
15
  '/api/chats': ['GET', 'POST'],
16
16
  '/api/chats/by-interaction/{interactionId}': ['GET'],
17
17
  '/api/chats/events': ['GET'],
18
+ '/api/chats/proactive/{eventId}': ['GET'],
18
19
  '/api/chats/{chatId}/shares': ['GET', 'POST'],
19
20
  '/api/chats/{id}': ['DELETE', 'GET', 'PATCH'],
20
21
  '/api/chats/{id}/generate-title': ['POST'],
package/src/index.ts CHANGED
@@ -324,6 +324,49 @@ export type {
324
324
  UserNotificationKind,
325
325
  } from "./user-notifications/index";
326
326
 
327
+ // Proactive delivery — how the system speaks first (ADR-CONTRACTS-142). A
328
+ // COMPOSER above the three notification vocabularies, not a fourth peer: the
329
+ // presentation class is a TOTAL FUNCTION from class to surfaces
330
+ // (CLASS_SURFACES, checked in both directions by the compiler), and a kind
331
+ // carries its class plus the IDS each surface needs — which banner, which chat
332
+ // template, which inbox kind — never the surface booleans. The born-read rule
333
+ // is stated once (bornRead) so the backend imports it rather than re-deriving
334
+ // it per call site. Pushed-chat prose lives here too (PROACTIVE_CHAT_COMPOSERS,
335
+ // total over the templates): the backend hands a composer facts and writes what
336
+ // comes back, FROZEN into the transcript — so a composer may only interpolate
337
+ // facts the recipient was entitled to at compose time. See src/proactive/README.md.
338
+ export {
339
+ PROACTIVE_PRESENTATION_CLASSES,
340
+ CLASS_SURFACES,
341
+ bornRead,
342
+ ORG_SYSTEM_EVENT_TYPES,
343
+ PROACTIVE_CHAT_TEMPLATES,
344
+ PROACTIVE_EVENT_KIND_IDS,
345
+ PROACTIVE_RECIPIENT_REASONS,
346
+ PROACTIVE_PROJECTIONS,
347
+ PROACTIVE_EVENT_KINDS,
348
+ PROACTIVE_CHAT_MAX_REPLIES,
349
+ PROACTIVE_CHAT_COMPOSERS,
350
+ } from "./proactive/index";
351
+
352
+ export type {
353
+ ProactivePresentationClass,
354
+ BadgeCarrier,
355
+ ClassSurfacePlan,
356
+ OrgSystemEventType,
357
+ ProactiveChatTemplate,
358
+ ProactiveEventKind,
359
+ ProactiveEventId,
360
+ ProactiveRecipientReason,
361
+ ProactiveProjection,
362
+ ProactiveEventDefinition,
363
+ OrgBecameSharedFacts,
364
+ MorningBriefFacts,
365
+ ProactiveChatFacts,
366
+ ProactiveChatComposer,
367
+ ProactiveChatMessage,
368
+ } from "./proactive/index";
369
+
327
370
  // Comment wire contract — where a thread hangs (the anchor union), what a
328
371
  // thread and its comments look like on the wire, and who a comment may name.
329
372
  // The anchor is the single most load-bearing shape in this package: the backend
@@ -481,11 +524,21 @@ export type {
481
524
  // Runtime profile types (PRD-00229)
482
525
  ChatRuntimeProfile,
483
526
  ChatRuntimeProfileInfo,
527
+ // Proactive chat wire vocabulary (ADR-CONTRACTS-142): how a chat was born,
528
+ // the occurrence-to-chat resolution states, and the recognized form of the
529
+ // wire-lenient `proactiveKind` string
530
+ ChatOrigin,
531
+ ProactiveChatResolution,
532
+ RecognizedProactiveKind,
484
533
  } from "./chat/index";
485
534
 
486
535
  export {
487
536
  CHAT_RUNTIME_PROFILES,
488
537
  DEFAULT_CHAT_RUNTIME_PROFILE,
538
+ // `proactiveKind` is a bare string on the wire so a new kind cannot make an
539
+ // older client reject the whole chat list; this is the ONE place it becomes
540
+ // a ProactiveEventKind. See src/chat/proactive-kind.ts.
541
+ recognizeProactiveKind,
489
542
  } from "./chat/index";
490
543
 
491
544
  // Organization domain types
@@ -961,6 +1014,11 @@ export type {
961
1014
  InteractiveTaskData,
962
1015
  InteractiveTaskPart,
963
1016
  InteractiveTaskDataPart,
1017
+ // Suggested replies surface types (non-governed chips, PRD-00958)
1018
+ SuggestedReply,
1019
+ SuggestedRepliesData,
1020
+ SuggestedRepliesPart,
1021
+ SuggestedRepliesDataPart,
964
1022
  // Execution result types (Phase 5)
965
1023
  ExecutionArtifactStatus,
966
1024
  ExecutionResultSummary,