@happyvertical/smrt-chat 0.42.6 → 0.43.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.
- package/AGENTS.md +7 -0
- package/README.md +19 -0
- package/dist/chunks/data-surface-bridge-BJomMxjS.js +740 -0
- package/dist/chunks/data-surface-bridge-BJomMxjS.js.map +1 -0
- package/dist/data-surface-bridge.d.ts +186 -0
- package/dist/data-surface-bridge.d.ts.map +1 -0
- package/dist/data-surface-bridge.js +2 -0
- package/dist/data-surface-normalizer.d.ts +11 -0
- package/dist/data-surface-normalizer.d.ts.map +1 -0
- package/dist/data-surface-tools.d.ts +9 -0
- package/dist/data-surface-tools.d.ts.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/index.js.map +1 -1
- package/dist/manifest.json +106 -22
- package/dist/smrt-knowledge.json +7 -5
- package/package.json +19 -11
package/dist/manifest.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"version": "1.0.0",
|
|
3
3
|
"timestamp": 0,
|
|
4
4
|
"packageName": "@happyvertical/smrt-chat",
|
|
5
|
-
"packageVersion": "0.
|
|
5
|
+
"packageVersion": "0.43.0",
|
|
6
6
|
"objects": {
|
|
7
7
|
"@happyvertical/smrt-chat:AgentSessionCollection": {
|
|
8
8
|
"name": "agentsessioncollection",
|
|
@@ -1438,7 +1438,13 @@
|
|
|
1438
1438
|
"type": "UUID",
|
|
1439
1439
|
"referenceKind": "foreignKey",
|
|
1440
1440
|
"notNull": false,
|
|
1441
|
-
"unique": false
|
|
1441
|
+
"unique": false,
|
|
1442
|
+
"foreignKey": {
|
|
1443
|
+
"table": "chat_rooms",
|
|
1444
|
+
"column": "id",
|
|
1445
|
+
"onDelete": "NO ACTION",
|
|
1446
|
+
"onUpdate": "CASCADE"
|
|
1447
|
+
}
|
|
1442
1448
|
},
|
|
1443
1449
|
"status": {
|
|
1444
1450
|
"type": "TEXT",
|
|
@@ -1534,7 +1540,7 @@
|
|
|
1534
1540
|
]
|
|
1535
1541
|
}
|
|
1536
1542
|
],
|
|
1537
|
-
"version": "
|
|
1543
|
+
"version": "43eaea36"
|
|
1538
1544
|
}
|
|
1539
1545
|
},
|
|
1540
1546
|
"@happyvertical/smrt-chat:ChatMessage": {
|
|
@@ -1872,13 +1878,25 @@
|
|
|
1872
1878
|
"type": "UUID",
|
|
1873
1879
|
"referenceKind": "foreignKey",
|
|
1874
1880
|
"notNull": true,
|
|
1875
|
-
"unique": false
|
|
1881
|
+
"unique": false,
|
|
1882
|
+
"foreignKey": {
|
|
1883
|
+
"table": "chat_rooms",
|
|
1884
|
+
"column": "id",
|
|
1885
|
+
"onDelete": "NO ACTION",
|
|
1886
|
+
"onUpdate": "CASCADE"
|
|
1887
|
+
}
|
|
1876
1888
|
},
|
|
1877
1889
|
"thread_id": {
|
|
1878
1890
|
"type": "UUID",
|
|
1879
1891
|
"referenceKind": "foreignKey",
|
|
1880
1892
|
"notNull": false,
|
|
1881
|
-
"unique": false
|
|
1893
|
+
"unique": false,
|
|
1894
|
+
"foreignKey": {
|
|
1895
|
+
"table": "chat_threads",
|
|
1896
|
+
"column": "id",
|
|
1897
|
+
"onDelete": "NO ACTION",
|
|
1898
|
+
"onUpdate": "CASCADE"
|
|
1899
|
+
}
|
|
1882
1900
|
},
|
|
1883
1901
|
"sender_profile_id": {
|
|
1884
1902
|
"type": "UUID",
|
|
@@ -1890,7 +1908,13 @@
|
|
|
1890
1908
|
"type": "UUID",
|
|
1891
1909
|
"referenceKind": "foreignKey",
|
|
1892
1910
|
"notNull": false,
|
|
1893
|
-
"unique": false
|
|
1911
|
+
"unique": false,
|
|
1912
|
+
"foreignKey": {
|
|
1913
|
+
"table": "agent_sessions",
|
|
1914
|
+
"column": "id",
|
|
1915
|
+
"onDelete": "NO ACTION",
|
|
1916
|
+
"onUpdate": "CASCADE"
|
|
1917
|
+
}
|
|
1894
1918
|
},
|
|
1895
1919
|
"content": {
|
|
1896
1920
|
"type": "TEXT",
|
|
@@ -1931,7 +1955,13 @@
|
|
|
1931
1955
|
"type": "UUID",
|
|
1932
1956
|
"referenceKind": "foreignKey",
|
|
1933
1957
|
"notNull": false,
|
|
1934
|
-
"unique": false
|
|
1958
|
+
"unique": false,
|
|
1959
|
+
"foreignKey": {
|
|
1960
|
+
"table": "chat_messages",
|
|
1961
|
+
"column": "id",
|
|
1962
|
+
"onDelete": "NO ACTION",
|
|
1963
|
+
"onUpdate": "CASCADE"
|
|
1964
|
+
}
|
|
1935
1965
|
},
|
|
1936
1966
|
"metadata": {
|
|
1937
1967
|
"type": "TEXT",
|
|
@@ -1999,7 +2029,7 @@
|
|
|
1999
2029
|
]
|
|
2000
2030
|
}
|
|
2001
2031
|
],
|
|
2002
|
-
"version": "
|
|
2032
|
+
"version": "cca14e75"
|
|
2003
2033
|
}
|
|
2004
2034
|
},
|
|
2005
2035
|
"@happyvertical/smrt-chat:ChatParticipant": {
|
|
@@ -2248,7 +2278,13 @@
|
|
|
2248
2278
|
"type": "UUID",
|
|
2249
2279
|
"referenceKind": "foreignKey",
|
|
2250
2280
|
"notNull": true,
|
|
2251
|
-
"unique": false
|
|
2281
|
+
"unique": false,
|
|
2282
|
+
"foreignKey": {
|
|
2283
|
+
"table": "chat_rooms",
|
|
2284
|
+
"column": "id",
|
|
2285
|
+
"onDelete": "NO ACTION",
|
|
2286
|
+
"onUpdate": "CASCADE"
|
|
2287
|
+
}
|
|
2252
2288
|
},
|
|
2253
2289
|
"profile_id": {
|
|
2254
2290
|
"type": "UUID",
|
|
@@ -2278,7 +2314,13 @@
|
|
|
2278
2314
|
"type": "UUID",
|
|
2279
2315
|
"referenceKind": "foreignKey",
|
|
2280
2316
|
"notNull": false,
|
|
2281
|
-
"unique": false
|
|
2317
|
+
"unique": false,
|
|
2318
|
+
"foreignKey": {
|
|
2319
|
+
"table": "chat_messages",
|
|
2320
|
+
"column": "id",
|
|
2321
|
+
"onDelete": "NO ACTION",
|
|
2322
|
+
"onUpdate": "CASCADE"
|
|
2323
|
+
}
|
|
2282
2324
|
},
|
|
2283
2325
|
"last_seen_at": {
|
|
2284
2326
|
"type": "TIMESTAMP",
|
|
@@ -2345,7 +2387,7 @@
|
|
|
2345
2387
|
]
|
|
2346
2388
|
}
|
|
2347
2389
|
],
|
|
2348
|
-
"version": "
|
|
2390
|
+
"version": "07714046"
|
|
2349
2391
|
}
|
|
2350
2392
|
},
|
|
2351
2393
|
"@happyvertical/smrt-chat:ChatReaction": {
|
|
@@ -2477,7 +2519,13 @@
|
|
|
2477
2519
|
"type": "UUID",
|
|
2478
2520
|
"referenceKind": "foreignKey",
|
|
2479
2521
|
"notNull": true,
|
|
2480
|
-
"unique": false
|
|
2522
|
+
"unique": false,
|
|
2523
|
+
"foreignKey": {
|
|
2524
|
+
"table": "chat_messages",
|
|
2525
|
+
"column": "id",
|
|
2526
|
+
"onDelete": "NO ACTION",
|
|
2527
|
+
"onUpdate": "CASCADE"
|
|
2528
|
+
}
|
|
2481
2529
|
},
|
|
2482
2530
|
"profile_id": {
|
|
2483
2531
|
"type": "UUID",
|
|
@@ -2522,7 +2570,7 @@
|
|
|
2522
2570
|
]
|
|
2523
2571
|
}
|
|
2524
2572
|
],
|
|
2525
|
-
"version": "
|
|
2573
|
+
"version": "5b1969a6"
|
|
2526
2574
|
}
|
|
2527
2575
|
},
|
|
2528
2576
|
"@happyvertical/smrt-chat:ChatRoom": {
|
|
@@ -3021,13 +3069,25 @@
|
|
|
3021
3069
|
"type": "UUID",
|
|
3022
3070
|
"referenceKind": "foreignKey",
|
|
3023
3071
|
"notNull": true,
|
|
3024
|
-
"unique": false
|
|
3072
|
+
"unique": false,
|
|
3073
|
+
"foreignKey": {
|
|
3074
|
+
"table": "chat_rooms",
|
|
3075
|
+
"column": "id",
|
|
3076
|
+
"onDelete": "NO ACTION",
|
|
3077
|
+
"onUpdate": "CASCADE"
|
|
3078
|
+
}
|
|
3025
3079
|
},
|
|
3026
3080
|
"root_message_id": {
|
|
3027
3081
|
"type": "UUID",
|
|
3028
3082
|
"referenceKind": "foreignKey",
|
|
3029
3083
|
"notNull": false,
|
|
3030
|
-
"unique": false
|
|
3084
|
+
"unique": false,
|
|
3085
|
+
"foreignKey": {
|
|
3086
|
+
"table": "chat_messages",
|
|
3087
|
+
"column": "id",
|
|
3088
|
+
"onDelete": "NO ACTION",
|
|
3089
|
+
"onUpdate": "CASCADE"
|
|
3090
|
+
}
|
|
3031
3091
|
},
|
|
3032
3092
|
"title": {
|
|
3033
3093
|
"type": "TEXT",
|
|
@@ -3089,7 +3149,7 @@
|
|
|
3089
3149
|
]
|
|
3090
3150
|
}
|
|
3091
3151
|
],
|
|
3092
|
-
"version": "
|
|
3152
|
+
"version": "e1ae958b"
|
|
3093
3153
|
}
|
|
3094
3154
|
},
|
|
3095
3155
|
"@happyvertical/smrt-chat:VoiceGatewayTurn": {
|
|
@@ -3288,7 +3348,13 @@
|
|
|
3288
3348
|
"type": "UUID",
|
|
3289
3349
|
"referenceKind": "foreignKey",
|
|
3290
3350
|
"notNull": true,
|
|
3291
|
-
"unique": false
|
|
3351
|
+
"unique": false,
|
|
3352
|
+
"foreignKey": {
|
|
3353
|
+
"table": "voice_sessions",
|
|
3354
|
+
"column": "id",
|
|
3355
|
+
"onDelete": "CASCADE",
|
|
3356
|
+
"onUpdate": "CASCADE"
|
|
3357
|
+
}
|
|
3292
3358
|
},
|
|
3293
3359
|
"gateway_session_id": {
|
|
3294
3360
|
"type": "TEXT",
|
|
@@ -3349,7 +3415,7 @@
|
|
|
3349
3415
|
]
|
|
3350
3416
|
}
|
|
3351
3417
|
],
|
|
3352
|
-
"version": "
|
|
3418
|
+
"version": "c4d871b8"
|
|
3353
3419
|
}
|
|
3354
3420
|
},
|
|
3355
3421
|
"@happyvertical/smrt-chat:VoiceSession": {
|
|
@@ -3734,19 +3800,37 @@
|
|
|
3734
3800
|
"type": "UUID",
|
|
3735
3801
|
"referenceKind": "foreignKey",
|
|
3736
3802
|
"notNull": true,
|
|
3737
|
-
"unique": false
|
|
3803
|
+
"unique": false,
|
|
3804
|
+
"foreignKey": {
|
|
3805
|
+
"table": "agent_sessions",
|
|
3806
|
+
"column": "id",
|
|
3807
|
+
"onDelete": "NO ACTION",
|
|
3808
|
+
"onUpdate": "CASCADE"
|
|
3809
|
+
}
|
|
3738
3810
|
},
|
|
3739
3811
|
"chat_room_id": {
|
|
3740
3812
|
"type": "UUID",
|
|
3741
3813
|
"referenceKind": "foreignKey",
|
|
3742
3814
|
"notNull": true,
|
|
3743
|
-
"unique": false
|
|
3815
|
+
"unique": false,
|
|
3816
|
+
"foreignKey": {
|
|
3817
|
+
"table": "chat_rooms",
|
|
3818
|
+
"column": "id",
|
|
3819
|
+
"onDelete": "NO ACTION",
|
|
3820
|
+
"onUpdate": "CASCADE"
|
|
3821
|
+
}
|
|
3744
3822
|
},
|
|
3745
3823
|
"thread_id": {
|
|
3746
3824
|
"type": "UUID",
|
|
3747
3825
|
"referenceKind": "foreignKey",
|
|
3748
3826
|
"notNull": false,
|
|
3749
|
-
"unique": false
|
|
3827
|
+
"unique": false,
|
|
3828
|
+
"foreignKey": {
|
|
3829
|
+
"table": "chat_threads",
|
|
3830
|
+
"column": "id",
|
|
3831
|
+
"onDelete": "NO ACTION",
|
|
3832
|
+
"onUpdate": "CASCADE"
|
|
3833
|
+
}
|
|
3750
3834
|
},
|
|
3751
3835
|
"target": {
|
|
3752
3836
|
"type": "TEXT",
|
|
@@ -3848,7 +3932,7 @@
|
|
|
3848
3932
|
]
|
|
3849
3933
|
}
|
|
3850
3934
|
],
|
|
3851
|
-
"version": "
|
|
3935
|
+
"version": "d5cfb90c"
|
|
3852
3936
|
}
|
|
3853
3937
|
}
|
|
3854
3938
|
},
|
package/dist/smrt-knowledge.json
CHANGED
|
@@ -3,17 +3,19 @@
|
|
|
3
3
|
"sensitiveFieldsExcluded": true,
|
|
4
4
|
"generatedAt": "1970-01-01T00:00:00.000Z",
|
|
5
5
|
"packageName": "@happyvertical/smrt-chat",
|
|
6
|
-
"packageVersion": "0.
|
|
6
|
+
"packageVersion": "0.43.0",
|
|
7
7
|
"sourceManifestPath": "dist/manifest.json",
|
|
8
8
|
"agentDocPath": "AGENTS.md",
|
|
9
9
|
"sourceHashes": {
|
|
10
|
-
"manifest": "
|
|
11
|
-
"packageJson": "
|
|
12
|
-
"agents": "
|
|
10
|
+
"manifest": "28dbe311b990649db9d3b720e86db114a152389a9b2c01c062a0dc9e6caf1c83",
|
|
11
|
+
"packageJson": "23a27448b6e1da0c484b6003529887fae33130055b1fdaaf798f0f5646d2de1d",
|
|
12
|
+
"agents": "0b73f5655366eb0aaee625ce18bccb56425a0720534f9248691a2950f6c68573"
|
|
13
13
|
},
|
|
14
14
|
"exports": [
|
|
15
15
|
".",
|
|
16
16
|
"./client",
|
|
17
|
+
"./data-surface",
|
|
18
|
+
"./data-surface-bridge",
|
|
17
19
|
"./internal/agent-runtime",
|
|
18
20
|
"./manifest",
|
|
19
21
|
"./manifest.json",
|
|
@@ -2735,5 +2737,5 @@
|
|
|
2735
2737
|
"polymorphicAssociations": 0,
|
|
2736
2738
|
"uuidColumns": 46
|
|
2737
2739
|
},
|
|
2738
|
-
"agentDoc": "# @happyvertical/smrt-chat\n\nChat rooms, threads, and agent sessions with app-controlled tool whitelisting.\n\n## Dev Server\n\n`pnpm --dir packages/chat dev` runs a package-local SvelteKit workbench. The root\nroute is an interactive chat surface with a dev-only `/api/dev-chat` endpoint:\nit uses `@happyvertical/ai` when local provider credentials are present and\nfalls back to a deterministic local assistant otherwise. `/api/dev-chat-stream`\nis its SSE companion (#1936) — the same provider/local-fallback resolution wired\nthrough `createChatStreamHandler` in plain mode, so an embedded `SmrtChatBackend`\nclient can exercise token streaming locally. `/previews` hosts the shared\ncomponent playground entries from `src/svelte/playground.ts`.\n\nThe root workbench also has a dev-only voice conversation mode. It reads voice\ngateway connection details through `/api/dev-voice/config`, streams browser mic\naudio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to\nthe chat, and plays returned TTS audio. Exposing\n`SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires\n`SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.\n\n## Models\n\nInternal models — all mutations go through the membership/owner-checked `ChatService` (S5 #1392) or the voice adapter's binding-checked flow. EVERY `@smrt()` model in this package (ChatRoom, ChatMessage, ChatParticipant, ChatThread, ChatReaction, AgentSession, VoiceSession) has a READ-ONLY generated REST/MCP surface (`list`/`get` only); `create`/`update`/`delete` are intentionally NOT generated so the raw collection routes cannot skip the service-layer authorization. A structural regression test enumerates the registry to assert no chat model exposes a mutating op.\n\n`ChatService` is a CLOSED FACADE (S5 #1392). The raw collections (`rooms`, `messages`, `participants`, `threads`, `agentSessions`, `reactions`, `voiceSessions`) are ES `#private` fields — they are NOT on the public `ChatService` type and the package index does NOT export the collection classes, so a consumer cannot do `chat.messages.create({senderProfileId, role})` / `new ChatParticipantCollection(...)` to mutate around the authorization. The security-sensitive internals (`#writeMessage`, `#emitAgentReply`, `#enrollParticipant`, `#loadActiveSession`, `#requireActiveMembership`, `#requireRoomAdmin`, `#extractToolName`) are ES `#private` too, so they are unreachable at runtime — TypeScript `private` alone is erased and would leave them callable on the prototype. The agent-reply bridge is a `Symbol`-keyed static (not the old enumerable `_runAgentReply`), reachable only by the module-local `sendAgentReply` that holds the non-exported symbol.\n\n- **ChatRoom**: `roomType` (public/private/dm/agent), `status`, `topic`, `maxParticipants`, `lastMessageAt`. Tenant-scoped (required).\n- **ChatMessage**: shared by users + agents. `role` (user/assistant/system/tool), `messageType` (text/system/action/file/tool_call/tool_result), `toolCallData` JSON. Unified model — no separate agent message type. Tenant-scoped (required).\n- **ChatParticipant**: `role` (owner/admin/member/viewer), `onlineStatus`, `lastReadMessageId`, `isMuted`. Tenant-scoped (required).\n- **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).\n- **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).\n- **AgentSession**: `agentId` (string ref, not FK), `allowedTools` (JSON string array), `sessionContext` (JSON), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.\n- **VoiceSession**: short-lived voice-gateway binding over `(tenant, actorProfileId, personaId, room/thread/agentSession)` plus a persona snapshot, gateway `session_id`, expiry, replay tracking, and metadata. Tenant-scoped (required). Generated surface is read-only; creation and turns go through `createVoiceChatSession()` / `handleVoiceGatewayTurn()`.\n\n## ChatService\n\nEvery public write takes an explicit server-supplied `actorProfileId` (the authenticated principal the route injects) — never a caller-controlled `senderProfileId`/`role` (S5 #1392).\n\nFacade: `sendMessage()` (authors as the actor with `role: 'user'`; room-membership-checked; no caller-supplied sender/role and no public membership-skip), `createRoom()` (acting actor becomes owner — no caller-supplied `createdByProfileId`), `startThread()` (member-checked; optional `rootMessageId` bound to the same room+tenant), `addParticipant()`/`removeParticipant()` (owner/admin-checked; self-leave allowed), `updateRoom()` (owner/admin-checked), `addReaction()`/`removeReaction()` (member-checked, self-keyed), `getOrCreateDM()` (actor must be a DM participant), `createAgentSession()` (acting actor becomes the session participant — no caller-supplied `participantProfileId`; the existing-session room lookup is tenant-bound; optional `sessionKey` scopes session identity to a conversation subject so distinct keys get distinct sessions/rooms and a session opened for one subject is never reused/rewritten for another). Tenant-bound read facade (replaces raw-collection reach-ins; consumers apply their own ownership/context checks on the returned rows): `getAgentSession({agentSessionId, tenantId})`, `findActiveAgentSessions({tenantId, agentId, participantProfileId})`, `getThread({threadId, tenantId})`, `listRoomThreads({roomId, actorProfileId, tenantId})` (membership-gated), `getThreadMessages({threadId, actorProfileId, tenantId, limit?})` (membership-gated, chronological), `getRoomMessages({roomId, actorProfileId, tenantId})`/`getRoomForMember(roomId, actorProfileId, tenantId)` (membership-checked reads gated on the server-supplied `actorProfileId`, never a caller-controlled subject id — confused-deputy avoidance; `tenantId` required), `updateAgentSessionConfig()` (owner-checked; `tenantId` mandatory and bound into the lookup). Agent session messaging is split by authority: `sendAgentUserMessage()` (caller `actorProfileId` must be the session participant; always authored as the participant). The agent-authored reply path `sendAgentReply(service, params)` is an exported **function — NOT a `ChatService` method and NOT on the package index**; it is reachable only via the dedicated `@happyvertical/smrt-chat/internal/agent-runtime` subpath (S5 #1392), so only trusted in-process agent-runtime code that explicitly opts into that subpath can author as the agent. It authors as `session.agentId`, accepts an optional same-room/tenant `threadId`, and gates tool calls fail-closed against `allowedTools`. The shared internal persistence path (`writeMessage`) is private — it alone may author an arbitrary profile/role or skip the membership check, and is unreachable from any route; it also validates every supplied `threadId`/`agentSessionId`/`replyToMessageId` belongs to the SAME room AND tenant (tenant/room-bound lookups) before use, rejecting cross-room/cross-tenant references. Auto-creates rooms/sessions/participants via an internal `enrollParticipant`.\n\n## Agent Tool Whitelisting\n\n`allowedTools` is a JSON array controlled by the consuming app. Fail-closed: an empty/unparseable whitelist permits NO tools. The internal `sendAgentReply(service, params)` function enforces the whitelist before emitting any `tool`/`tool_call` message; a caller cannot supply a `senderProfileId`/`role` to post as the agent, and the function is not reachable from the package index.\n\n## Conversational Harness (L3, #1891)\n\nThe \"chat with your learning agent\" surface — the real agentic runtime for `AgentSession` (the only shipping chat runtime before this was a single-shot completion). This is the new **acyclic `chat → personas` / `chat → agents` / `chat → users` edge**; keep it that way (personas/agents/users never depend back on chat).\n\n- **`runToolLoop(options)`** (`tool-loop.ts`) — a bounded `tool_call → observe → respond` loop. Tools are **manifest operations** of installed packages: `buildManifestToolCatalog({ allowedTools })` reads the `PermissionCatalogService` catalog and keeps only the `(collection, action)` entries named in the persona's allow-list (the **offer gate**; absent/empty ⇒ NO tools). The loop runs inside one `executeAsPrincipal` context, and `invokeManifestTool` executes each op **in-process (\"side door\")** against `run.context.database` (the RLS tx when Postgres RLS is on), after re-asserting the fail-closed allow-list (`run.assertToolAllowed`) AND the catalog permission (`run.assertOperation`) — the **execution gate**. Bounded by a max-steps ceiling (`DEFAULT_MAX_STEPS = 8`): on the ceiling it disables tools for one final completion so the turn always terminates with text.\n- **`runPersonaConversationTurn(options)`** (`persona-conversation.ts`) — binds a conversation to an `AgentPersona`/`ResolvedPersona`: runs as its principal (`runAsUserId`), offers only its `allowedTools`, speaks its instructions (`resolvePersonaInstructions`, layering approved learned directives), and injects its **recalled learning memory** (`personaLearningMemory`, isolated per `memoryScope`) into the system prompt. `bindPersonaToSession()` mirrors the persona's `allowedTools`/instructions onto the `AgentSession` so the chat authoring gate agrees with the loop's offer gate. Authors the reply (and each executed tool) via the internal `sendAgentReply` bridge.\n- **Agent orchestration** (L3, #1892) — the loop accepts non-manifest **`extraTools`** (`PrincipalTool[]`, from `@happyvertical/smrt-agents`), gated by the *same* fail-closed allow-list. The standard **`invoke-agent`** tool (`createInvokeAgentTool`, slug `agents.invoke`) lets a conversational agent delegate to a **worker agent under its own principal** — the worker runs via `executeAsPrincipal` as the originating user (never its own authority), the principal is immutable along the chain, and its completion is surfaced back into the conversation. `runPersonaConversationTurn` filters `extraTools` by the persona's `allowedTools` (offer gate); the tool's `execute` re-asserts `assertToolAllowed` (execution gate). See `@happyvertical/smrt-agents` for the delegation envelope + transports.\n- **Chat feedback capture** (`chat-feedback.ts`) — `captureChatFeedback()` + `acceptAppliedChange`/`rejectAppliedChange`/`correctResponse`/`rateResponse`/`thumbsUp`/`thumbsDown` write a `Feedback` row (personas) carrying the conversation's **correlation-id**, and (by default) reinforce the persona's learning memory (`reinforceFromFeedback`). So an in-chat reject decays a strategy below the reuse floor and it stops being recalled; a correction supersedes its stored value.\n\n## Voice Gateway Turns (#1910)\n\nVoice is an input mode for the existing persona chat harness, not a separate chat runtime. `createVoiceChatSession()` creates a short-lived `VoiceSession` for an authenticated actor/profile, binding tenant, persona, agent session, room, and optional thread. `handleVoiceGatewayTurn()` resolves that binding from `metadata.voiceSessionId`, checks the gateway's `session_id` and any supplied tenant/profile/persona/session/thread metadata against the server-side binding, persists the transcript through `ChatService.sendAgentUserMessage()`, runs `runPersonaConversationTurn()`, stamps voice/correlation metadata onto the persisted user/assistant/tool messages, records the gateway `turn_id`, and returns the gateway response contract. The Fetch-compatible `createVoiceGatewayTurnHandler()` adds the coarse gateway bearer-token check.\n\nThe gateway bearer token proves only \"this request came from the gateway\"; it never authorizes the end user. The short-lived `VoiceSession` binding is the user/session proof, and untrusted gateway metadata must be validated against that binding before any chat write or tool loop. Tool execution remains fail-closed through the persona allow-list mirrored onto `AgentSession` by `bindPersonaToSession()`.\n\n## Token Streaming (SSE, #1936)\n\n`chat-stream.ts` is the SSE seam for embeddable conversational UIs (first consumer: the Happy chat widget, `animation#5`): a client POSTs the conversation so far and receives a `text/event-stream` of `data: <json>` frames — `token` deltas as the model generates, then a final `done` frame with the message. The wire `ChatStreamEvent` union also declares `emotion` and `control` (#1921 host-page control commands) lanes for forward compatibility; the v1 engine emits `token`/`done`/`error`.\n\n- **`runChatConversationStream({ context, messages })`** — the transport-agnostic engine (an `AsyncGenerator<ChatStreamEvent>`). Dispatches on `context.binding`: **persona-bound** runs the full `runPersonaConversationTurn` with a token sink wired through the tool loop (`onToken` → `ai.chat({ stream: true, onProgress })`), then persists via `ChatService` and emits the persisted message as `done`; **plain/unbound** streams `ai.stream()` directly and emits a synthesized (unpersisted) `done`. Streamed tokens are a live PREVIEW (a tool-call round may narrate before acting); the `done` message is authoritative. Failures surface as an in-band `error` event, never a throw (the 200 has already committed once streaming starts).\n- **`createChatStreamHandler({ authorize, allowedOrigins?, allowCredentials? })`** — a Fetch-compatible handler returning `text/event-stream` (mirrors `createVoiceGatewayTurnHandler`). `authorize(request, body)` is the SOLE trust boundary and works exactly like the voice gateway: this module NEVER authorizes from the request's `session` metadata — the app validates the caller (bearer session id / cookie / same-origin) and the claimed ids against the authenticated principal, and returns an already-authorized `ChatStreamContext`. Generation caps (`model`/`maxTokens`/`maxSteps`) live on the context (server-resolved), never on the request. Cross-origin embedding uses the same fail-closed CORS posture as core `_events` (#1861): the `Origin` is echoed only when allow-listed (never `*`), credentials only when opted in.\n- **`SmrtChatBackend` (`@happyvertical/smrt-chat/client`)** — the consume side of the same contract: a browser SSE client (`src/client.ts`) that POSTs the conversation and dispatches `token`/`emotion`/`done`/`error` frames to streaming handlers, tolerating heartbeat comments and frames split across chunks. The subpath is BROWSER-SAFE and dependency-free (no server runtime, no workspace imports — keep it that way), and its widget-facing types are structurally identical to `@happyvertical/animation`'s `ChatBackend` contract so an instance plugs straight into the floating chat widget. `src/client.contract.ts` carries the compile-time locks pinning it to `chat-stream.ts`'s `ChatStreamEvent`/`ChatStreamSession` — a NON-test module precisely so `pnpm typecheck` actually enforces them (`tsconfig.typecheck.json` excludes `.test.ts` files, and Vitest transpiles without typechecking). A clean close without a `done` frame surfaces as an error (never an empty reply), and a settled turn cancels the reader so the connection is released promptly.\n- **Persona path reuses the harness's own gates unchanged** — persona principal, fail-closed `allowedTools` offer+execution gates, tenant binding. `onToken` is best-effort telemetry threaded through `runToolLoop`; it never changes what the loop persists or authorizes.\n- **Custom tools stream via `binding.extraTools`** — the persona binding threads an optional `extraTools?: PrincipalTool[]` down to `runPersonaConversationTurn`, so a *streamed* persona chat can offer non-manifest, service-backed tools (the persona messaging tool `messages.send`, or an assistance-request/lead-ticket tool wrapping a `@smrt({ api:false, mcp:false })` service) and thus *act*, not only answer — matching the non-streaming persona path. It is resolved server-side by `authorize` (trusted), never from request input, and stays fully gated: each tool is filtered by the persona's `allowedTools` (offer gate) and re-asserts the bound principal's authority in `execute` (execution gate). Offering a tool is not authorizing it.\n\n## Gotchas\n\n- **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.\n- **Agent rooms auto-created**: `roomType: 'agent'`, `maxParticipants` defaults to 2; the agent is enrolled as a member so its replies pass the membership check. `createAgentSession()` re-enrolls the participant AND the agent on the existing-session path, so legacy sessions created before the agent was enrolled self-heal.\n- **Per-subject sessions need `sessionKey`**: `createAgentSession()` reuses ANY active session for the same `(agentId, participantProfileId, tenantId)`. Callers that open separate conversations per subject (e.g. one content-editor session per content id) MUST pass a stable `sessionKey` (stored in `sessionContext.__sessionKey`, read via `AgentSession.getSessionKey()`); otherwise a session opened for one subject is reused and its context overwritten for another, surfacing the wrong room/threads (S5 #1392). A keyed create never reuses a keyless/legacy session.\n- **Session expiry**: check `isActive()` before allowing messages (expiresAt or limit-based)\n- **DM identity**: derived from the deterministic per-tenant `canonicalDmRoomId()` and the authoritative `chat_participants` join, not client metadata; concurrent creates upsert onto one row.\n- **Tenant-bound lookups**: membership/session/DM lookups REQUIRE `tenantId` and always bind it into the WHERE clause (`findActiveMembership`/`isActiveMember`/`findActiveSession` take a required `tenantId`; AgentSession's `null` tenant is an explicit bound scope, not \"any tenant\") so they can never resolve a row from another tenant.\n"
|
|
2740
|
+
"agentDoc": "# @happyvertical/smrt-chat\n\nChat rooms, threads, and agent sessions with app-controlled tool whitelisting.\n\n## Dev Server\n\n`pnpm --dir packages/chat dev` runs a package-local SvelteKit workbench. The root\nroute is an interactive chat surface with a dev-only `/api/dev-chat` endpoint:\nit uses `@happyvertical/ai` when local provider credentials are present and\nfalls back to a deterministic local assistant otherwise. `/api/dev-chat-stream`\nis its SSE companion (#1936) — the same provider/local-fallback resolution wired\nthrough `createChatStreamHandler` in plain mode, so an embedded `SmrtChatBackend`\nclient can exercise token streaming locally. `/previews` hosts the shared\ncomponent playground entries from `src/svelte/playground.ts`.\n\nThe root workbench also has a dev-only voice conversation mode. It reads voice\ngateway connection details through `/api/dev-voice/config`, streams browser mic\naudio to `WS /ws/voice` as PCM16 mono, appends gateway transcripts/responses to\nthe chat, and plays returned TTS audio. Exposing\n`SMRT_CHAT_DEV_VOICE_GATEWAY_TOKEN` to the browser requires\n`SMRT_CHAT_DEV_VOICE_GATEWAY_EXPOSE_TOKEN=true`; keep that local-only.\n\n## Models\n\nInternal models — all mutations go through the membership/owner-checked `ChatService` (S5 #1392) or the voice adapter's binding-checked flow. EVERY `@smrt()` model in this package (ChatRoom, ChatMessage, ChatParticipant, ChatThread, ChatReaction, AgentSession, VoiceSession) has a READ-ONLY generated REST/MCP surface (`list`/`get` only); `create`/`update`/`delete` are intentionally NOT generated so the raw collection routes cannot skip the service-layer authorization. A structural regression test enumerates the registry to assert no chat model exposes a mutating op.\n\n`ChatService` is a CLOSED FACADE (S5 #1392). The raw collections (`rooms`, `messages`, `participants`, `threads`, `agentSessions`, `reactions`, `voiceSessions`) are ES `#private` fields — they are NOT on the public `ChatService` type and the package index does NOT export the collection classes, so a consumer cannot do `chat.messages.create({senderProfileId, role})` / `new ChatParticipantCollection(...)` to mutate around the authorization. The security-sensitive internals (`#writeMessage`, `#emitAgentReply`, `#enrollParticipant`, `#loadActiveSession`, `#requireActiveMembership`, `#requireRoomAdmin`, `#extractToolName`) are ES `#private` too, so they are unreachable at runtime — TypeScript `private` alone is erased and would leave them callable on the prototype. The agent-reply bridge is a `Symbol`-keyed static (not the old enumerable `_runAgentReply`), reachable only by the module-local `sendAgentReply` that holds the non-exported symbol.\n\n- **ChatRoom**: `roomType` (public/private/dm/agent), `status`, `topic`, `maxParticipants`, `lastMessageAt`. Tenant-scoped (required).\n- **ChatMessage**: shared by users + agents. `role` (user/assistant/system/tool), `messageType` (text/system/action/file/tool_call/tool_result), `toolCallData` JSON. Unified model — no separate agent message type. Tenant-scoped (required).\n- **ChatParticipant**: `role` (owner/admin/member/viewer), `onlineStatus`, `lastReadMessageId`, `isMuted`. Tenant-scoped (required).\n- **ChatThread**: `rootMessageId`, `isResolved`, `messageCount`. Created via `ChatService.startThread()` (member-checked). Tenant-scoped (required).\n- **ChatReaction**: `messageId`, `profileId`, `emoji`. Added/removed via `ChatService.addReaction()`/`removeReaction()` (member-checked, self-keyed). Tenant-scoped (required).\n- **AgentSession**: `agentId` (string ref, not FK), `allowedTools` (JSON string array), `sessionContext` (JSON), `systemPrompt`, limits (`maxTokens`/`maxMessages`/`expiresAt`). Optional tenancy.\n- **VoiceSession**: short-lived voice-gateway binding over `(tenant, actorProfileId, personaId, room/thread/agentSession)` plus a persona snapshot, gateway `session_id`, expiry, replay tracking, and metadata. Tenant-scoped (required). Generated surface is read-only; creation and turns go through `createVoiceChatSession()` / `handleVoiceGatewayTurn()`.\n\n## ChatService\n\nEvery public write takes an explicit server-supplied `actorProfileId` (the authenticated principal the route injects) — never a caller-controlled `senderProfileId`/`role` (S5 #1392).\n\nFacade: `sendMessage()` (authors as the actor with `role: 'user'`; room-membership-checked; no caller-supplied sender/role and no public membership-skip), `createRoom()` (acting actor becomes owner — no caller-supplied `createdByProfileId`), `startThread()` (member-checked; optional `rootMessageId` bound to the same room+tenant), `addParticipant()`/`removeParticipant()` (owner/admin-checked; self-leave allowed), `updateRoom()` (owner/admin-checked), `addReaction()`/`removeReaction()` (member-checked, self-keyed), `getOrCreateDM()` (actor must be a DM participant), `createAgentSession()` (acting actor becomes the session participant — no caller-supplied `participantProfileId`; the existing-session room lookup is tenant-bound; optional `sessionKey` scopes session identity to a conversation subject so distinct keys get distinct sessions/rooms and a session opened for one subject is never reused/rewritten for another). Tenant-bound read facade (replaces raw-collection reach-ins; consumers apply their own ownership/context checks on the returned rows): `getAgentSession({agentSessionId, tenantId})`, `findActiveAgentSessions({tenantId, agentId, participantProfileId})`, `getThread({threadId, tenantId})`, `listRoomThreads({roomId, actorProfileId, tenantId})` (membership-gated), `getThreadMessages({threadId, actorProfileId, tenantId, limit?})` (membership-gated, chronological), `getRoomMessages({roomId, actorProfileId, tenantId})`/`getRoomForMember(roomId, actorProfileId, tenantId)` (membership-checked reads gated on the server-supplied `actorProfileId`, never a caller-controlled subject id — confused-deputy avoidance; `tenantId` required), `updateAgentSessionConfig()` (owner-checked; `tenantId` mandatory and bound into the lookup). Agent session messaging is split by authority: `sendAgentUserMessage()` (caller `actorProfileId` must be the session participant; always authored as the participant). The agent-authored reply path `sendAgentReply(service, params)` is an exported **function — NOT a `ChatService` method and NOT on the package index**; it is reachable only via the dedicated `@happyvertical/smrt-chat/internal/agent-runtime` subpath (S5 #1392), so only trusted in-process agent-runtime code that explicitly opts into that subpath can author as the agent. It authors as `session.agentId`, accepts an optional same-room/tenant `threadId`, and gates tool calls fail-closed against `allowedTools`. The shared internal persistence path (`writeMessage`) is private — it alone may author an arbitrary profile/role or skip the membership check, and is unreachable from any route; it also validates every supplied `threadId`/`agentSessionId`/`replyToMessageId` belongs to the SAME room AND tenant (tenant/room-bound lookups) before use, rejecting cross-room/cross-tenant references. Auto-creates rooms/sessions/participants via an internal `enrollParticipant`.\n\n## Agent Tool Whitelisting\n\n`allowedTools` is a JSON array controlled by the consuming app. Fail-closed: an empty/unparseable whitelist permits NO tools. The internal `sendAgentReply(service, params)` function enforces the whitelist before emitting any `tool`/`tool_call` message; a caller cannot supply a `senderProfileId`/`role` to post as the agent, and the function is not reachable from the package index.\n\nPrincipal-bound data discovery and reads are available as `PrincipalTool[]` via\n`createDataSurfaceTools()` (re-exported by the chat package). Pass the tools as\n`extraTools` to the persona conversation/tool loop. They use the same\nfail-closed `allowedTools` offer/execution gates; RBAC, tenant, redaction,\nbounded query results, and audit authority remain in the authenticated\n`PrincipalRun` supplied by `@happyvertical/smrt-agents`.\n\n## Conversational Harness (L3, #1891)\n\nThe \"chat with your learning agent\" surface — the real agentic runtime for `AgentSession` (the only shipping chat runtime before this was a single-shot completion). This is the new **acyclic `chat → personas` / `chat → agents` / `chat → users` edge**; keep it that way (personas/agents/users never depend back on chat).\n\n- **`runToolLoop(options)`** (`tool-loop.ts`) — a bounded `tool_call → observe → respond` loop. Tools are **manifest operations** of installed packages: `buildManifestToolCatalog({ allowedTools })` reads the `PermissionCatalogService` catalog and keeps only the `(collection, action)` entries named in the persona's allow-list (the **offer gate**; absent/empty ⇒ NO tools). The loop runs inside one `executeAsPrincipal` context, and `invokeManifestTool` executes each op **in-process (\"side door\")** against `run.context.database` (the RLS tx when Postgres RLS is on), after re-asserting the fail-closed allow-list (`run.assertToolAllowed`) AND the catalog permission (`run.assertOperation`) — the **execution gate**. Bounded by a max-steps ceiling (`DEFAULT_MAX_STEPS = 8`): on the ceiling it disables tools for one final completion so the turn always terminates with text.\n- **`runPersonaConversationTurn(options)`** (`persona-conversation.ts`) — binds a conversation to an `AgentPersona`/`ResolvedPersona`: runs as its principal (`runAsUserId`), offers only its `allowedTools`, speaks its instructions (`resolvePersonaInstructions`, layering approved learned directives), and injects its **recalled learning memory** (`personaLearningMemory`, isolated per `memoryScope`) into the system prompt. `bindPersonaToSession()` mirrors the persona's `allowedTools`/instructions onto the `AgentSession` so the chat authoring gate agrees with the loop's offer gate. Authors the reply (and each executed tool) via the internal `sendAgentReply` bridge.\n- **Agent orchestration** (L3, #1892) — the loop accepts non-manifest **`extraTools`** (`PrincipalTool[]`, from `@happyvertical/smrt-agents`), gated by the *same* fail-closed allow-list. The standard **`invoke-agent`** tool (`createInvokeAgentTool`, slug `agents.invoke`) lets a conversational agent delegate to a **worker agent under its own principal** — the worker runs via `executeAsPrincipal` as the originating user (never its own authority), the principal is immutable along the chain, and its completion is surfaced back into the conversation. `runPersonaConversationTurn` filters `extraTools` by the persona's `allowedTools` (offer gate); the tool's `execute` re-asserts `assertToolAllowed` (execution gate). See `@happyvertical/smrt-agents` for the delegation envelope + transports.\n- **Chat feedback capture** (`chat-feedback.ts`) — `captureChatFeedback()` + `acceptAppliedChange`/`rejectAppliedChange`/`correctResponse`/`rateResponse`/`thumbsUp`/`thumbsDown` write a `Feedback` row (personas) carrying the conversation's **correlation-id**, and (by default) reinforce the persona's learning memory (`reinforceFromFeedback`). So an in-chat reject decays a strategy below the reuse floor and it stops being recalled; a correction supersedes its stored value.\n\n## Voice Gateway Turns (#1910)\n\nVoice is an input mode for the existing persona chat harness, not a separate chat runtime. `createVoiceChatSession()` creates a short-lived `VoiceSession` for an authenticated actor/profile, binding tenant, persona, agent session, room, and optional thread. `handleVoiceGatewayTurn()` resolves that binding from `metadata.voiceSessionId`, checks the gateway's `session_id` and any supplied tenant/profile/persona/session/thread metadata against the server-side binding, persists the transcript through `ChatService.sendAgentUserMessage()`, runs `runPersonaConversationTurn()`, stamps voice/correlation metadata onto the persisted user/assistant/tool messages, records the gateway `turn_id`, and returns the gateway response contract. The Fetch-compatible `createVoiceGatewayTurnHandler()` adds the coarse gateway bearer-token check.\n\nThe gateway bearer token proves only \"this request came from the gateway\"; it never authorizes the end user. The short-lived `VoiceSession` binding is the user/session proof, and untrusted gateway metadata must be validated against that binding before any chat write or tool loop. Tool execution remains fail-closed through the persona allow-list mirrored onto `AgentSession` by `bindPersonaToSession()`.\n\n## Token Streaming (SSE, #1936)\n\n`chat-stream.ts` is the SSE seam for embeddable conversational UIs (first consumer: the Happy chat widget, `animation#5`): a client POSTs the conversation so far and receives a `text/event-stream` of `data: <json>` frames — `token` deltas as the model generates, then a final `done` frame with the message. The wire `ChatStreamEvent` union also declares `emotion` and `control` (#1921 host-page control commands) lanes for forward compatibility; the v1 engine emits `token`/`done`/`error`.\n\n- **`runChatConversationStream({ context, messages })`** — the transport-agnostic engine (an `AsyncGenerator<ChatStreamEvent>`). Dispatches on `context.binding`: **persona-bound** runs the full `runPersonaConversationTurn` with a token sink wired through the tool loop (`onToken` → `ai.chat({ stream: true, onProgress })`), then persists via `ChatService` and emits the persisted message as `done`; **plain/unbound** streams `ai.stream()` directly and emits a synthesized (unpersisted) `done`. Streamed tokens are a live PREVIEW (a tool-call round may narrate before acting); the `done` message is authoritative. Failures surface as an in-band `error` event, never a throw (the 200 has already committed once streaming starts).\n- **`createChatStreamHandler({ authorize, allowedOrigins?, allowCredentials? })`** — a Fetch-compatible handler returning `text/event-stream` (mirrors `createVoiceGatewayTurnHandler`). `authorize(request, body)` is the SOLE trust boundary and works exactly like the voice gateway: this module NEVER authorizes from the request's `session` metadata — the app validates the caller (bearer session id / cookie / same-origin) and the claimed ids against the authenticated principal, and returns an already-authorized `ChatStreamContext`. Generation caps (`model`/`maxTokens`/`maxSteps`) live on the context (server-resolved), never on the request. Cross-origin embedding uses the same fail-closed CORS posture as core `_events` (#1861): the `Origin` is echoed only when allow-listed (never `*`), credentials only when opted in.\n- **`SmrtChatBackend` (`@happyvertical/smrt-chat/client`)** — the consume side of the same contract: a browser SSE client (`src/client.ts`) that POSTs the conversation and dispatches `token`/`emotion`/`done`/`error` frames to streaming handlers, tolerating heartbeat comments and frames split across chunks. The subpath is BROWSER-SAFE and dependency-free (no server runtime, no workspace imports — keep it that way), and its widget-facing types are structurally identical to `@happyvertical/animation`'s `ChatBackend` contract so an instance plugs straight into the floating chat widget. `src/client.contract.ts` carries the compile-time locks pinning it to `chat-stream.ts`'s `ChatStreamEvent`/`ChatStreamSession` — a NON-test module precisely so `pnpm typecheck` actually enforces them (`tsconfig.typecheck.json` excludes `.test.ts` files, and Vitest transpiles without typechecking). A clean close without a `done` frame surfaces as an error (never an empty reply), and a settled turn cancels the reader so the connection is released promptly.\n- **Persona path reuses the harness's own gates unchanged** — persona principal, fail-closed `allowedTools` offer+execution gates, tenant binding. `onToken` is best-effort telemetry threaded through `runToolLoop`; it never changes what the loop persists or authorizes.\n- **Custom tools stream via `binding.extraTools`** — the persona binding threads an optional `extraTools?: PrincipalTool[]` down to `runPersonaConversationTurn`, so a *streamed* persona chat can offer non-manifest, service-backed tools (the persona messaging tool `messages.send`, or an assistance-request/lead-ticket tool wrapping a `@smrt({ api:false, mcp:false })` service) and thus *act*, not only answer — matching the non-streaming persona path. It is resolved server-side by `authorize` (trusted), never from request input, and stays fully gated: each tool is filtered by the persona's `allowedTools` (offer gate) and re-asserts the bound principal's authority in `execute` (execution gate). Offering a tool is not authorizing it.\n\n## Gotchas\n\n- **sessionContext, not context**: `context` is reserved for slug scoping. Use `getSessionContext()`/`updateSessionContext()` for agent memory.\n- **Agent rooms auto-created**: `roomType: 'agent'`, `maxParticipants` defaults to 2; the agent is enrolled as a member so its replies pass the membership check. `createAgentSession()` re-enrolls the participant AND the agent on the existing-session path, so legacy sessions created before the agent was enrolled self-heal.\n- **Per-subject sessions need `sessionKey`**: `createAgentSession()` reuses ANY active session for the same `(agentId, participantProfileId, tenantId)`. Callers that open separate conversations per subject (e.g. one content-editor session per content id) MUST pass a stable `sessionKey` (stored in `sessionContext.__sessionKey`, read via `AgentSession.getSessionKey()`); otherwise a session opened for one subject is reused and its context overwritten for another, surfacing the wrong room/threads (S5 #1392). A keyed create never reuses a keyless/legacy session.\n- **Session expiry**: check `isActive()` before allowing messages (expiresAt or limit-based)\n- **DM identity**: derived from the deterministic per-tenant `canonicalDmRoomId()` and the authoritative `chat_participants` join, not client metadata; concurrent creates upsert onto one row.\n- **Tenant-bound lookups**: membership/session/DM lookups REQUIRE `tenantId` and always bind it into the WHERE clause (`findActiveMembership`/`isActiveMember`/`findActiveSession` take a required `tenantId`; AgentSession's `null` tenant is an explicit bound scope, not \"any tenant\") so they can never resolve a row from another tenant.\n"
|
|
2739
2741
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@happyvertical/smrt-chat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"description": "Chat rooms, DMs, threads, and agent conversations for the SMRT framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"smrtRawPrimitives": "strict",
|
|
@@ -15,6 +15,14 @@
|
|
|
15
15
|
"types": "./dist/client.d.ts",
|
|
16
16
|
"import": "./dist/client.js"
|
|
17
17
|
},
|
|
18
|
+
"./data-surface-bridge": {
|
|
19
|
+
"types": "./dist/data-surface-bridge.d.ts",
|
|
20
|
+
"import": "./dist/data-surface-bridge.js"
|
|
21
|
+
},
|
|
22
|
+
"./data-surface": {
|
|
23
|
+
"types": "./dist/data-surface-bridge.d.ts",
|
|
24
|
+
"import": "./dist/data-surface-bridge.js"
|
|
25
|
+
},
|
|
18
26
|
"./ui": {
|
|
19
27
|
"types": "./dist/ui.d.ts",
|
|
20
28
|
"import": "./dist/ui.js"
|
|
@@ -62,13 +70,13 @@
|
|
|
62
70
|
"dependencies": {
|
|
63
71
|
"@happyvertical/ai": "^0.88.0",
|
|
64
72
|
"@happyvertical/sql": "^0.88.0",
|
|
65
|
-
"@happyvertical/smrt-agents": "0.
|
|
66
|
-
"@happyvertical/smrt-core": "0.
|
|
67
|
-
"@happyvertical/smrt-personas": "0.
|
|
68
|
-
"@happyvertical/smrt-types": "0.
|
|
69
|
-
"@happyvertical/smrt-tenancy": "0.
|
|
70
|
-
"@happyvertical/smrt-ui": "0.
|
|
71
|
-
"@happyvertical/smrt-users": "0.
|
|
73
|
+
"@happyvertical/smrt-agents": "0.43.0",
|
|
74
|
+
"@happyvertical/smrt-core": "0.43.0",
|
|
75
|
+
"@happyvertical/smrt-personas": "0.43.0",
|
|
76
|
+
"@happyvertical/smrt-types": "0.43.0",
|
|
77
|
+
"@happyvertical/smrt-tenancy": "0.43.0",
|
|
78
|
+
"@happyvertical/smrt-ui": "0.43.0",
|
|
79
|
+
"@happyvertical/smrt-users": "0.43.0"
|
|
72
80
|
},
|
|
73
81
|
"peerDependencies": {
|
|
74
82
|
"svelte": "^5.56.4"
|
|
@@ -89,9 +97,9 @@
|
|
|
89
97
|
"typescript": "5.9.3",
|
|
90
98
|
"vite": "8.1.4",
|
|
91
99
|
"vitest": "4.1.10",
|
|
92
|
-
"@happyvertical/smrt-
|
|
93
|
-
"@happyvertical/smrt-
|
|
94
|
-
"@happyvertical/smrt-
|
|
100
|
+
"@happyvertical/smrt-playground": "0.43.0",
|
|
101
|
+
"@happyvertical/smrt-vitest": "0.43.0",
|
|
102
|
+
"@happyvertical/smrt-profiles": "0.43.0"
|
|
95
103
|
},
|
|
96
104
|
"scripts": {
|
|
97
105
|
"build": "SMRT_PACKAGE_BUILD=1 vite build --mode library && SMRT_PACKAGE_BUILD=1 svelte-package -i src/svelte -o dist/svelte --tsconfig tsconfig.svelte.json",
|