@ai-matrx/chat 0.4.8 → 0.4.10
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/CHANGELOG.md +28 -0
- package/dist/agents/redux/execution-system/thunks/resolve-base-url.js +17 -0
- package/dist/agents/redux/execution-system/thunks/run-headless-agent-json.js +14 -2
- package/dist/agents/routing/confidential-routing.d.ts +52 -0
- package/dist/agents/routing/confidential-routing.js +38 -0
- package/dist/canvas/workspace/ChatPanelTitleMenu.d.ts +3 -1
- package/dist/canvas/workspace/ChatPanelTitleMenu.js +66 -34
- package/dist/canvas/workspace/ShellChatDock.js +1 -1
- package/dist/canvas/workspace/shell-chat-route.d.ts +9 -3
- package/dist/canvas/workspace/shell-chat-route.js +6 -1
- package/dist/host/configure.js +4 -1
- package/dist/host/contract.d.ts +14 -1
- package/dist/host/index.d.ts +1 -0
- package/dist/host/index.js +1 -0
- package/package.json +8 -8
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,33 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.4.10 — a confidential conversation never reaches a cloud target
|
|
4
|
+
|
|
5
|
+
- THE CONFIDENTIAL-ROUTING RULE moves from the frozen kernel (`src/compat`) into the real chat:
|
|
6
|
+
`agents/routing/confidential-routing.ts` (pure: `decideRouting`, `localityOfChannel`,
|
|
7
|
+
`ConfidentialRoutingRefusedError`), also exported from `@ai-matrx/chat/host`.
|
|
8
|
+
- New host port `routing: { policy(conversationId) => "any" | "local-only" }` (default `"any"`).
|
|
9
|
+
- `resolveBackendForConversation` — the one place every send path resolves its target — refuses a
|
|
10
|
+
`local-only` conversation on every channel but `local-runtime` (the Matrx Local engine on this
|
|
11
|
+
computer), including the cloud fallback when a local-pc conversation finds no engine. It throws
|
|
12
|
+
`ConfidentialRoutingRefusedError` (message = the sentence the person reads, `remedy`, `code`
|
|
13
|
+
`confidential_thread_cloud_target_refused`) before any request is made.
|
|
14
|
+
- Forcing test `agents/routing/__tests__/confidential-conversation-never-reaches-the-cloud.test.ts`
|
|
15
|
+
(red on the unfixed resolver; three plant.py mutations red).
|
|
16
|
+
|
|
17
|
+
Consumer action: none for hosts without confidential conversations (the default policy is `"any"`).
|
|
18
|
+
A host with confidential conversations (matrx-local) passes `routing` to its chat host.
|
|
19
|
+
|
|
20
|
+
## 0.4.9 — the shell chat remembers each page's own chat, and its title menu lists your conversations
|
|
21
|
+
|
|
22
|
+
- `shellChatHome`: outside boards and Education, the conversation is remembered PER PAGE
|
|
23
|
+
(`shellChatPageSurfaceKey(pathname)`), not one conversation following the person site-wide. A page
|
|
24
|
+
never chatted on starts fresh; open / closed stays remembered per page family.
|
|
25
|
+
- `ChatPanelTitleMenu`: a Popover with New chat · Rename · Open in full chat over the person's
|
|
26
|
+
conversations (`ConversationHistorySidebar`, searchable, opened in place). New required prop
|
|
27
|
+
`onOpenConversation(conversationId, agentId)`; the shell dock passes `openExisting`.
|
|
28
|
+
|
|
29
|
+
Consumer action: none (the dock is inside the package).
|
|
30
|
+
|
|
3
31
|
## 0.4.8
|
|
4
32
|
|
|
5
33
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -14,6 +14,12 @@ import {
|
|
|
14
14
|
supportsLocalAgentExecution
|
|
15
15
|
} from "../../../../host/server/local-engine.js";
|
|
16
16
|
import { selectOrganizationId } from "../../../../host/org.js";
|
|
17
|
+
import { getChatHost, isChatHostConfigured } from "../../../../host/configure.js";
|
|
18
|
+
import {
|
|
19
|
+
ConfidentialRoutingRefusedError,
|
|
20
|
+
decideRouting,
|
|
21
|
+
localityOfChannel
|
|
22
|
+
} from "../../../routing/confidential-routing.js";
|
|
17
23
|
const EC2_SANDBOX_SERVER_URL = typeof process !== "undefined" && process.env.NEXT_PUBLIC_EC2_SANDBOX_SERVER_URL || "";
|
|
18
24
|
function dedicatedEc2ServerForConversation(state, conversationId) {
|
|
19
25
|
if (selectActiveServer(state) !== "production") return null;
|
|
@@ -65,6 +71,17 @@ function resolveBaseUrlForConversation(state, conversationId) {
|
|
|
65
71
|
return resolved.endsWith("/") ? resolved.slice(0, -1) : resolved;
|
|
66
72
|
}
|
|
67
73
|
function resolveBackendForConversation(state, conversationId) {
|
|
74
|
+
const resolved = resolveBackendUnchecked(state, conversationId);
|
|
75
|
+
if (resolved && isChatHostConfigured()) {
|
|
76
|
+
const decision = decideRouting(
|
|
77
|
+
getChatHost().routing.policy(conversationId),
|
|
78
|
+
localityOfChannel(resolved.channel)
|
|
79
|
+
);
|
|
80
|
+
if (!decision.allowed) throw new ConfidentialRoutingRefusedError(conversationId, decision);
|
|
81
|
+
}
|
|
82
|
+
return resolved;
|
|
83
|
+
}
|
|
84
|
+
function resolveBackendUnchecked(state, conversationId) {
|
|
68
85
|
const entry = state.instanceUIState?.byConversationId?.[conversationId] ?? null;
|
|
69
86
|
const overrideUrl = entry?.serverOverrideUrl ?? null;
|
|
70
87
|
const headers = {
|
|
@@ -151,7 +151,8 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
|
|
|
151
151
|
}
|
|
152
152
|
const who = holderName ?? (warning.holderType === "workflow" ? "The chosen workflow" : "The chosen agent");
|
|
153
153
|
const keys = missing.join(", ");
|
|
154
|
-
const
|
|
154
|
+
const brokenJson = !(result.success && isRecord) && looksLikeBrokenJson(result.fullResponse);
|
|
155
|
+
const sentence = brokenJson ? `${who} ran, but its answer came back as broken JSON, so it couldn't be read and nothing was saved. Run it again.` : `${who} ran, but its answer is missing ${keys} this job needs, so nothing was saved. It was chosen although it does not declare those keys \u2014 pick one that does.`;
|
|
155
156
|
captureError({
|
|
156
157
|
source: "agent-json-result",
|
|
157
158
|
code: "mandate_output_unusable",
|
|
@@ -162,7 +163,7 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
|
|
|
162
163
|
...result.requestId ? { requestId: result.requestId } : {},
|
|
163
164
|
...result.conversationId ? { conversationId: result.conversationId } : {},
|
|
164
165
|
raw: {
|
|
165
|
-
defect: "warned-holder-answer-missing-required-keys",
|
|
166
|
+
defect: brokenJson ? "answer-is-broken-json" : "warned-holder-answer-missing-required-keys",
|
|
166
167
|
mandateKey: opts.mandateKey,
|
|
167
168
|
surfaceKey: opts.surfaceKey,
|
|
168
169
|
holderId,
|
|
@@ -179,6 +180,17 @@ async function failWarnedOutputMissingKeys(getState, opts, result) {
|
|
|
179
180
|
errorDetail: `mandate_output_unusable: missing ${keys}`
|
|
180
181
|
};
|
|
181
182
|
}
|
|
183
|
+
function looksLikeBrokenJson(text) {
|
|
184
|
+
if (!text) return false;
|
|
185
|
+
const t = text.trim().replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "");
|
|
186
|
+
if (!t.startsWith("{") && !t.startsWith("[")) return false;
|
|
187
|
+
try {
|
|
188
|
+
JSON.parse(t);
|
|
189
|
+
return false;
|
|
190
|
+
} catch {
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
182
194
|
function missingRequiredKeys(record, required) {
|
|
183
195
|
const answers = typeof record.answers === "object" && record.answers !== null && !Array.isArray(record.answers) ? record.answers : null;
|
|
184
196
|
return required.filter((key) => !(key in record) && !(answers && key in answers));
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* THE CONFIDENTIAL-ROUTING RULE (CHAT-UI-PACKAGE-DESIGN §12 risk 4 — the highest-severity item).
|
|
3
|
+
*
|
|
4
|
+
* A conversation pinned `local-only` (matrx-local's "Confidential") REFUSES every target that is
|
|
5
|
+
* not the person's own machine. It does not downgrade, it does not warn-and-send, and it never
|
|
6
|
+
* quietly picks a different target: the send fails with a named reason and a remedy, before a byte
|
|
7
|
+
* leaves the machine. Re-deriving this per host is how private text reaches a cloud provider.
|
|
8
|
+
*
|
|
9
|
+
* Ported from the frozen kernel (`src/compat/config.ts` `decideRouting`) into the real chat
|
|
10
|
+
* (lanes/agents-chat-proposal.md W0(a)). Enforced at the one place every send path resolves its
|
|
11
|
+
* target: `resolveBackendForConversation` (`agents/redux/execution-system/thunks/resolve-base-url.ts`).
|
|
12
|
+
* The policy comes from the host's `routing` port (`host/contract.ts`; default: `"any"`).
|
|
13
|
+
*
|
|
14
|
+
* PURE on purpose: no imports, no React, no Redux, no `window`. The decision is headless so it can
|
|
15
|
+
* move (e.g. into `@ai-matrx/agents`) without a rewrite.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* Where a conversation's text is allowed to go.
|
|
19
|
+
* - `"any"` — the platform default: any target the host offers.
|
|
20
|
+
* - `"local-only"` — CONFIDENTIAL: only the person's own machine (the Matrx Local engine).
|
|
21
|
+
*/
|
|
22
|
+
export type ChatRoutingPolicy = "any" | "local-only";
|
|
23
|
+
/** Where a resolved target sends text. */
|
|
24
|
+
export type ChatTargetLocality = "local" | "cloud";
|
|
25
|
+
/** The refusal a confidential conversation produces instead of a silent fallback. */
|
|
26
|
+
export interface ChatRoutingRefusal {
|
|
27
|
+
readonly allowed: false;
|
|
28
|
+
readonly code: "confidential_thread_cloud_target_refused";
|
|
29
|
+
readonly userMessage: string;
|
|
30
|
+
readonly remedy: string;
|
|
31
|
+
}
|
|
32
|
+
export type ChatRoutingDecision = {
|
|
33
|
+
readonly allowed: true;
|
|
34
|
+
} | ChatRoutingRefusal;
|
|
35
|
+
export declare const CONFIDENTIAL_REFUSAL_MESSAGE = "This conversation is marked confidential, so it cannot be sent to a cloud AI provider.";
|
|
36
|
+
export declare const CONFIDENTIAL_REFUSAL_REMEDY = "Run it on Matrx Local on this computer, or start a new conversation that is not marked confidential.";
|
|
37
|
+
/** The one decision. Anything that is not `"local"` is treated as cloud by its callers. */
|
|
38
|
+
export declare function decideRouting(policy: ChatRoutingPolicy, target: ChatTargetLocality): ChatRoutingDecision;
|
|
39
|
+
/**
|
|
40
|
+
* The locality of a resolved backend channel. Only `"local-runtime"` — the Matrx Local engine on
|
|
41
|
+
* this computer — is local. Every other channel (the global server, a sandbox override, a dedicated
|
|
42
|
+
* EC2 server, and any channel added later) is cloud: an unknown channel can never pass the rule.
|
|
43
|
+
*/
|
|
44
|
+
export declare function localityOfChannel(channel: string): ChatTargetLocality;
|
|
45
|
+
/** Thrown by the target resolver when a confidential conversation would reach the cloud. */
|
|
46
|
+
export declare class ConfidentialRoutingRefusedError extends Error {
|
|
47
|
+
readonly code: "confidential_thread_cloud_target_refused";
|
|
48
|
+
readonly remedy: string;
|
|
49
|
+
readonly conversationId: string;
|
|
50
|
+
constructor(conversationId: string, refusal: ChatRoutingRefusal);
|
|
51
|
+
}
|
|
52
|
+
export declare function isConfidentialRoutingRefusedError(error: unknown): error is ConfidentialRoutingRefusedError;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const CONFIDENTIAL_REFUSAL_MESSAGE = "This conversation is marked confidential, so it cannot be sent to a cloud AI provider.";
|
|
2
|
+
const CONFIDENTIAL_REFUSAL_REMEDY = "Run it on Matrx Local on this computer, or start a new conversation that is not marked confidential.";
|
|
3
|
+
function decideRouting(policy, target) {
|
|
4
|
+
if (policy === "local-only" && target !== "local") {
|
|
5
|
+
return {
|
|
6
|
+
allowed: false,
|
|
7
|
+
code: "confidential_thread_cloud_target_refused",
|
|
8
|
+
userMessage: CONFIDENTIAL_REFUSAL_MESSAGE,
|
|
9
|
+
remedy: CONFIDENTIAL_REFUSAL_REMEDY
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
return { allowed: true };
|
|
13
|
+
}
|
|
14
|
+
function localityOfChannel(channel) {
|
|
15
|
+
return channel === "local-runtime" ? "local" : "cloud";
|
|
16
|
+
}
|
|
17
|
+
class ConfidentialRoutingRefusedError extends Error {
|
|
18
|
+
code = "confidential_thread_cloud_target_refused";
|
|
19
|
+
remedy;
|
|
20
|
+
conversationId;
|
|
21
|
+
constructor(conversationId, refusal) {
|
|
22
|
+
super(refusal.userMessage);
|
|
23
|
+
this.name = "ConfidentialRoutingRefusedError";
|
|
24
|
+
this.remedy = refusal.remedy;
|
|
25
|
+
this.conversationId = conversationId;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function isConfidentialRoutingRefusedError(error) {
|
|
29
|
+
return error instanceof ConfidentialRoutingRefusedError;
|
|
30
|
+
}
|
|
31
|
+
export {
|
|
32
|
+
CONFIDENTIAL_REFUSAL_MESSAGE,
|
|
33
|
+
CONFIDENTIAL_REFUSAL_REMEDY,
|
|
34
|
+
ConfidentialRoutingRefusedError,
|
|
35
|
+
decideRouting,
|
|
36
|
+
isConfidentialRoutingRefusedError,
|
|
37
|
+
localityOfChannel
|
|
38
|
+
};
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
/** The panel's chat name: its real title, else "New chat". */
|
|
2
2
|
export declare function useChatPanelTitle(conversationId: string | null): string;
|
|
3
|
-
export declare function ChatPanelTitleMenu({ conversationId, onNewChat, }: {
|
|
3
|
+
export declare function ChatPanelTitleMenu({ conversationId, onNewChat, onOpenConversation, }: {
|
|
4
4
|
conversationId: string | null;
|
|
5
5
|
onNewChat: () => void;
|
|
6
|
+
/** Open a listed conversation IN this panel. */
|
|
7
|
+
onOpenConversation: (conversationId: string, agentId?: string | null) => void;
|
|
6
8
|
}): import("react").JSX.Element;
|
|
@@ -1,20 +1,18 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
3
|
+
import { useState } from "react";
|
|
3
4
|
import { ChevronDown, ExternalLink, PencilLine, Plus } from "lucide-react";
|
|
4
5
|
import { AppLink } from "../../host/ui-slots.js";
|
|
5
|
-
import {
|
|
6
|
-
DropdownMenu,
|
|
7
|
-
DropdownMenuContent,
|
|
8
|
-
DropdownMenuItem,
|
|
9
|
-
DropdownMenuSeparator,
|
|
10
|
-
DropdownMenuTrigger
|
|
11
|
-
} from "@ai-matrx/design-system";
|
|
6
|
+
import { Popover, PopoverContent, PopoverTrigger } from "@ai-matrx/design-system";
|
|
12
7
|
import { useAppSelector } from "../../store/hooks.js";
|
|
13
8
|
import { selectConversationTitle } from "../../agents/redux/execution-system/conversations/conversations.selectors.js";
|
|
14
9
|
import { selectConversationListItemById } from "../../agents/redux/conversation-list/conversation-list.selectors.js";
|
|
15
10
|
import { conversationRenameOpener } from "../../agents/components/conversation-actions/rename/conversationRenameOpener.js";
|
|
16
11
|
import { conversationTitleText } from "../../utils/content-ir/surfaces/kind-text-label.js";
|
|
17
|
-
import { Button } from "@ai-matrx/design-system/controls";
|
|
12
|
+
import { Button, Tile } from "@ai-matrx/design-system/controls";
|
|
13
|
+
import { ConversationHistorySidebar } from "../../agents/components/conversation-history/ConversationHistorySidebar.js";
|
|
14
|
+
const PANEL_HISTORY_SCOPE = "chat-panel-menu";
|
|
15
|
+
const ALL_AGENTS = [];
|
|
18
16
|
function useChatRealTitle(conversationId) {
|
|
19
17
|
const conversationTitle = useAppSelector(
|
|
20
18
|
(state) => conversationId ? selectConversationTitle(conversationId)(state) : null
|
|
@@ -29,35 +27,69 @@ function useChatPanelTitle(conversationId) {
|
|
|
29
27
|
}
|
|
30
28
|
function ChatPanelTitleMenu({
|
|
31
29
|
conversationId,
|
|
32
|
-
onNewChat
|
|
30
|
+
onNewChat,
|
|
31
|
+
onOpenConversation
|
|
33
32
|
}) {
|
|
33
|
+
const [open, setOpen] = useState(false);
|
|
34
34
|
const realTitle = useChatRealTitle(conversationId);
|
|
35
35
|
const title = realTitle ?? "New chat";
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/* @__PURE__ */
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
{
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
36
|
+
const close = () => setOpen(false);
|
|
37
|
+
return /* @__PURE__ */ jsxs(Popover, { open, onOpenChange: setOpen, children: [
|
|
38
|
+
/* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsx(Button, { variant: "quiet", iconEnd: /* @__PURE__ */ jsx(ChevronDown, {}), className: "min-w-0", title, children: title }) }),
|
|
39
|
+
/* @__PURE__ */ jsxs(
|
|
40
|
+
PopoverContent,
|
|
41
|
+
{
|
|
42
|
+
align: "start",
|
|
43
|
+
sideOffset: 6,
|
|
44
|
+
className: "flex h-[min(70dvh,560px)] w-80 flex-col overflow-hidden p-1",
|
|
45
|
+
children: [
|
|
46
|
+
/* @__PURE__ */ jsxs("div", { className: "flex shrink-0 flex-col", children: [
|
|
47
|
+
/* @__PURE__ */ jsx(
|
|
48
|
+
Tile,
|
|
49
|
+
{
|
|
50
|
+
variant: "quiet",
|
|
51
|
+
icon: /* @__PURE__ */ jsx(Plus, {}),
|
|
52
|
+
title: "New chat",
|
|
53
|
+
onClick: () => {
|
|
54
|
+
close();
|
|
55
|
+
onNewChat();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
),
|
|
59
|
+
conversationId ? /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
60
|
+
/* @__PURE__ */ jsx(
|
|
61
|
+
Tile,
|
|
62
|
+
{
|
|
63
|
+
variant: "quiet",
|
|
64
|
+
icon: /* @__PURE__ */ jsx(PencilLine, {}),
|
|
65
|
+
title: "Rename",
|
|
66
|
+
onClick: () => {
|
|
67
|
+
close();
|
|
68
|
+
void conversationRenameOpener.open({ conversationId, title: realTitle });
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
),
|
|
72
|
+
/* @__PURE__ */ jsx(Tile, { variant: "quiet", icon: /* @__PURE__ */ jsx(ExternalLink, {}), title: "Open in full chat", asChild: true, children: /* @__PURE__ */ jsx(AppLink, { href: `/chat/${conversationId}`, onClick: close }) })
|
|
73
|
+
] }) : null
|
|
74
|
+
] }),
|
|
75
|
+
/* @__PURE__ */ jsx("div", { className: "mx-1 my-1 h-px shrink-0 bg-border", role: "separator" }),
|
|
76
|
+
/* @__PURE__ */ jsx("div", { className: "flex min-h-0 flex-1 flex-col", children: /* @__PURE__ */ jsx(
|
|
77
|
+
ConversationHistorySidebar,
|
|
78
|
+
{
|
|
79
|
+
variant: "dense",
|
|
80
|
+
scopeId: PANEL_HISTORY_SCOPE,
|
|
81
|
+
agentIds: ALL_AGENTS,
|
|
82
|
+
activeConversationId: conversationId,
|
|
83
|
+
openInPlace: true,
|
|
84
|
+
onOpenConversation: (conv) => {
|
|
85
|
+
close();
|
|
86
|
+
onOpenConversation(conv.conversationId, conv.agentId ?? null);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
) })
|
|
90
|
+
]
|
|
91
|
+
}
|
|
92
|
+
)
|
|
61
93
|
] });
|
|
62
94
|
}
|
|
63
95
|
export {
|
|
@@ -260,7 +260,7 @@ function ShellChatDock({ initialChat, initialWidth, initialMode = null, signedIn
|
|
|
260
260
|
className: "bg-card",
|
|
261
261
|
children: [
|
|
262
262
|
/* @__PURE__ */ jsxs("div", { className: "shell-chat-dock-header flex h-11 shrink-0 items-center gap-1 pr-2", children: [
|
|
263
|
-
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(ChatPanelTitleMenu, { conversationId, onNewChat: chat.startNew }) }),
|
|
263
|
+
/* @__PURE__ */ jsx("div", { className: "min-w-0 flex-1", children: /* @__PURE__ */ jsx(ChatPanelTitleMenu, { conversationId, onNewChat: chat.startNew, onOpenConversation: chat.openExisting }) }),
|
|
264
264
|
/* @__PURE__ */ jsx("div", { className: "shrink-0", children: /* @__PURE__ */ jsx(ComposerModeSwitch, { size: "panel", initialMode }) }),
|
|
265
265
|
/* @__PURE__ */ jsx(
|
|
266
266
|
Button,
|
|
@@ -33,8 +33,14 @@ export declare function shellChatWorkspaceId(family: string): string;
|
|
|
33
33
|
* chat, on every page).
|
|
34
34
|
*/
|
|
35
35
|
export declare function shellChatHostedElsewhere(pathname: string): boolean;
|
|
36
|
-
/**
|
|
36
|
+
/**
|
|
37
|
+
* The shell chat's conversation is remembered PER PAGE (Arman, 2026-10-07: one
|
|
38
|
+
* conversation following the person across the whole site "doesn't make sense
|
|
39
|
+
* … it's confusing"). Each page — one agent's builder, one note — comes back to
|
|
40
|
+
* ITS last chat; a page the person never chatted on starts fresh.
|
|
41
|
+
*/
|
|
37
42
|
export declare const SHELL_CHAT_SURFACE_KEY = "canvas-workspace:shell";
|
|
43
|
+
export declare function shellChatPageSurfaceKey(pathname: string): string;
|
|
38
44
|
/**
|
|
39
45
|
* Where the shell's chat lives on a page:
|
|
40
46
|
* - `layoutId` — its remembered open / closed + docked / floating
|
|
@@ -61,8 +67,8 @@ export declare const SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES: readonly RegExp[];
|
|
|
61
67
|
* has its conversation, signed-in Education has one. The ids, params and
|
|
62
68
|
* defaults are exactly the ones those pages used when they drew their own chat
|
|
63
69
|
* column (ChatCanvasWorkspace, until 2026-10-05), so a person's remembered
|
|
64
|
-
* layout and conversation carry over untouched. Every other page
|
|
65
|
-
* conversation
|
|
70
|
+
* layout and conversation carry over untouched. Every other page remembers its
|
|
71
|
+
* OWN conversation (shellChatPageSurfaceKey); open / closed is remembered per family.
|
|
66
72
|
*/
|
|
67
73
|
export declare function shellChatHome(pathname: string, signedIn: boolean): ShellChatHome;
|
|
68
74
|
/**
|
|
@@ -20,6 +20,10 @@ function shellChatHostedElsewhere(pathname) {
|
|
|
20
20
|
return OWN_CHAT_PATHS.some((p) => p.test(pathname));
|
|
21
21
|
}
|
|
22
22
|
const SHELL_CHAT_SURFACE_KEY = "canvas-workspace:shell";
|
|
23
|
+
function shellChatPageSurfaceKey(pathname) {
|
|
24
|
+
const page = pathname.split("?")[0].replace(/\/+$/, "") || "/";
|
|
25
|
+
return `${SHELL_CHAT_SURFACE_KEY}:${page}`;
|
|
26
|
+
}
|
|
23
27
|
const BOARD_PAGE = /^\/board\/(?!all(?:\/|$))([^/]+)\/?$/;
|
|
24
28
|
const EDUCATION_PAGES = /^\/education(?:\/|$)/;
|
|
25
29
|
const SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES = [/^\/spaces(?:\/|$)/];
|
|
@@ -34,7 +38,7 @@ function shellChatHome(pathname, signedIn) {
|
|
|
34
38
|
}
|
|
35
39
|
return {
|
|
36
40
|
layoutId: shellChatWorkspaceId(shellChatFamily(pathname)),
|
|
37
|
-
surfaceKey:
|
|
41
|
+
surfaceKey: shellChatPageSurfaceKey(pathname),
|
|
38
42
|
// `pageChat`, not `chat`: /code keeps its own `?chat=`.
|
|
39
43
|
addressParam: "pageChat",
|
|
40
44
|
defaultOpen: SHELL_CHAT_CLOSED_BY_DEFAULT_PAGES.some((p) => p.test(pathname)) ? false : null
|
|
@@ -52,5 +56,6 @@ export {
|
|
|
52
56
|
shellChatFamily,
|
|
53
57
|
shellChatHome,
|
|
54
58
|
shellChatHostedElsewhere,
|
|
59
|
+
shellChatPageSurfaceKey,
|
|
55
60
|
shellChatWorkspaceId
|
|
56
61
|
};
|
package/dist/host/configure.js
CHANGED
|
@@ -44,8 +44,10 @@ const PORTS = [
|
|
|
44
44
|
"chrome",
|
|
45
45
|
"feedback",
|
|
46
46
|
"routes",
|
|
47
|
-
"canvas"
|
|
47
|
+
"canvas",
|
|
48
|
+
"routing"
|
|
48
49
|
];
|
|
50
|
+
const ANY_ROUTING = Object.freeze({ policy: () => "any" });
|
|
49
51
|
const EMPTY_REGISTRATIONS = Object.freeze({});
|
|
50
52
|
function assertDb(host) {
|
|
51
53
|
const db = host?.db;
|
|
@@ -127,6 +129,7 @@ function resolveChatHost(host) {
|
|
|
127
129
|
feedback: host.feedback ?? createDbFeedback(db, () => ref.identity),
|
|
128
130
|
routes: { ...DEFAULT_CHAT_ROUTES, ...host.routes },
|
|
129
131
|
canvas: host.canvas ?? createUnhostedCanvas(() => ref.diagnostics),
|
|
132
|
+
routing: host.routing ?? ANY_ROUTING,
|
|
130
133
|
overridden
|
|
131
134
|
});
|
|
132
135
|
return ref;
|
package/dist/host/contract.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type { ChatCanvasTabKind } from "./canvas-tabs.js";
|
|
|
25
25
|
import type { ChatWindowOpeners } from "./window-openers.js";
|
|
26
26
|
import type { DefaultChatServerApi, DefaultChatServerTypes } from "./defaults/server-api.js";
|
|
27
27
|
import type { ChatMessageSource } from "./source.js";
|
|
28
|
+
import type { ChatRoutingPolicy } from "../agents/routing/confidential-routing.js";
|
|
28
29
|
/**
|
|
29
30
|
* The connection contract (R10). Authenticated, RLS applies. Typed with the
|
|
30
31
|
* package's own `ChatDatabase` (`./db-types`, generated: the schemas the
|
|
@@ -731,6 +732,15 @@ export interface ChatAppIdentity {
|
|
|
731
732
|
/** `source_feature` for every conversation this app starts; absent keeps each screen's own. */
|
|
732
733
|
sourceFeature?: string;
|
|
733
734
|
}
|
|
735
|
+
/**
|
|
736
|
+
* Where each conversation's text may go (lanes/agents-chat-proposal.md W0(a)). The package enforces
|
|
737
|
+
* the answer at the one place every send resolves its target (`resolveBackendForConversation`): a
|
|
738
|
+
* `"local-only"` (confidential) conversation refuses every target but the Matrx Local engine on this
|
|
739
|
+
* computer, loudly, before a byte leaves the machine. Default: `"any"` for every conversation.
|
|
740
|
+
*/
|
|
741
|
+
export interface ChatRoutingPort {
|
|
742
|
+
policy(conversationId: string): ChatRoutingPolicy;
|
|
743
|
+
}
|
|
734
744
|
export interface ChatHost {
|
|
735
745
|
/**
|
|
736
746
|
* R10 connection contract. Authenticated, RLS applies. Required unless `source` is given: a
|
|
@@ -791,8 +801,10 @@ export interface ChatHost {
|
|
|
791
801
|
routes?: Partial<ChatRoutes>;
|
|
792
802
|
/** Default: no canvas here — every open refuses and says so. */
|
|
793
803
|
canvas?: ChatCanvasPort;
|
|
804
|
+
/** Default: every conversation may go to any target (`"any"`). */
|
|
805
|
+
routing?: ChatRoutingPort;
|
|
794
806
|
}
|
|
795
|
-
export type ChatPortName = "identity" | "org" | "server" | "notify" | "diagnostics" | "prefs" | "navigation" | "windows" | "catalog" | "registry" | "chrome" | "feedback" | "routes" | "canvas";
|
|
807
|
+
export type ChatPortName = "identity" | "org" | "server" | "notify" | "diagnostics" | "prefs" | "navigation" | "windows" | "catalog" | "registry" | "chrome" | "feedback" | "routes" | "canvas" | "routing";
|
|
796
808
|
/** Every port present. `overridden` names the ports the host supplied itself. */
|
|
797
809
|
export interface ResolvedChatHost {
|
|
798
810
|
/** The host's client, or the no-database stand-in when it gave only a `source`. */
|
|
@@ -818,5 +830,6 @@ export interface ResolvedChatHost {
|
|
|
818
830
|
feedback: ChatFeedbackPort;
|
|
819
831
|
routes: ChatRoutes;
|
|
820
832
|
canvas: ChatCanvasPort;
|
|
833
|
+
routing: ChatRoutingPort;
|
|
821
834
|
overridden: ReadonlySet<ChatPortName>;
|
|
822
835
|
}
|
package/dist/host/index.d.ts
CHANGED
|
@@ -24,3 +24,4 @@ export { createClient, getClaimsUser, schedulerDb, supabase, type ChatClaimsUser
|
|
|
24
24
|
export type { ChatDatabase } from "./db-types.js";
|
|
25
25
|
export { ChatNoDatabaseError, NO_DATABASE_CODE_PREFIX, createNoDatabaseStandIn, isChatNoDatabaseError, isNoDatabase, } from "./no-database.js";
|
|
26
26
|
export * from "./source.js";
|
|
27
|
+
export * from "../agents/routing/confidential-routing.js";
|
package/dist/host/index.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.10",
|
|
4
4
|
"description": "THE one AI Matrx person-to-agent chat system: the full chat (running agents, conversations, tool calls, streaming, context, surfaces, voice) as per-module subpaths @ai-matrx/chat/<domain>/<module> over a host contract (/host, /host/react) whose only required value is a db; plus the kernel compatibility entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) until P28.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.cjs",
|
|
@@ -228,12 +228,15 @@
|
|
|
228
228
|
"zod": "^4.6.5",
|
|
229
229
|
"@ai-matrx/agents": "latest",
|
|
230
230
|
"@ai-matrx/alchemy": "latest",
|
|
231
|
-
"@ai-matrx/browser-audio": "latest",
|
|
232
|
-
"@ai-matrx/content-ir": "latest",
|
|
233
231
|
"@ai-matrx/associations": "latest",
|
|
234
232
|
"@ai-matrx/canvas": "latest",
|
|
233
|
+
"@ai-matrx/browser-audio": "latest",
|
|
234
|
+
"@ai-matrx/content-ir": "latest",
|
|
235
|
+
"@ai-matrx/content-ir-react": "latest",
|
|
236
|
+
"@ai-matrx/data": "latest",
|
|
235
237
|
"@ai-matrx/design-system": "latest",
|
|
236
238
|
"@ai-matrx/diff": "latest",
|
|
239
|
+
"@ai-matrx/icons": "latest",
|
|
237
240
|
"@ai-matrx/kit": "latest",
|
|
238
241
|
"@ai-matrx/media": "latest",
|
|
239
242
|
"@ai-matrx/print": "latest",
|
|
@@ -241,11 +244,8 @@
|
|
|
241
244
|
"@ai-matrx/records": "latest",
|
|
242
245
|
"@ai-matrx/records-ui": "latest",
|
|
243
246
|
"@ai-matrx/rich-content": "latest",
|
|
244
|
-
"@ai-matrx/
|
|
245
|
-
"@ai-matrx/
|
|
246
|
-
"@ai-matrx/content-ir-react": "latest",
|
|
247
|
-
"@ai-matrx/icons": "latest",
|
|
248
|
-
"@ai-matrx/tap-target": "latest"
|
|
247
|
+
"@ai-matrx/tap-target": "latest",
|
|
248
|
+
"@ai-matrx/rich-editor": "latest"
|
|
249
249
|
},
|
|
250
250
|
"devDependencies": {
|
|
251
251
|
"@arethetypeswrong/cli": "^0.18.5",
|