@ai-matrx/chat 0.7.20 → 0.7.22
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 +33 -0
- package/dist/agents/components/inputs/smart-input/ConversationContextRail.js +6 -1
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.d.ts +1 -1
- package/dist/agents/components/inputs/smart-input/composer/ComposerSplash.js +7 -5
- package/dist/agents/identity/agent-identity.js +25 -5
- package/dist/agents/redux/agent-definition/thunks.js +3 -1
- package/dist/agents/redux/execution-system/conversations/conversations.selectors.d.ts +9 -4
- package/dist/agents/redux/execution-system/conversations/conversations.selectors.js +7 -2
- package/dist/agents/redux/execution-system/utils/ids.d.ts +2 -0
- package/dist/agents/redux/execution-system/utils/ids.js +9 -2
- package/dist/mandates/service.js +68 -31
- package/dist/mandates/useMandateHolderSet.d.ts +9 -0
- package/dist/mandates/useMandateHolderSet.js +52 -0
- package/package.json +10 -10
package/CHANGELOG.md
CHANGED
|
@@ -1,11 +1,44 @@
|
|
|
1
1
|
# @ai-matrx/chat
|
|
2
2
|
|
|
3
|
+
## 0.7.22
|
|
4
|
+
|
|
5
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
|
+
`git diff npm/chat/v0.7.21..npm/chat/v0.7.22 -- apps/shared/chat`).
|
|
7
|
+
No source changes intended and no consumer action required.
|
|
8
|
+
|
|
9
|
+
## 0.7.21
|
|
10
|
+
### Unreleased
|
|
11
|
+
|
|
12
|
+
### /chat/new: the live residuals of the startup-read work
|
|
13
|
+
|
|
14
|
+
- **A minted id is a draft before its record exists.** `generateConversationId` remembers the ids this tab
|
|
15
|
+
minted (`wasMintedInThisTab`); `selectIsDraftConversation` answers true for one Redux does not hold yet.
|
|
16
|
+
The composer mounts — and its children's effects run — before the launch thunk creates the record, so
|
|
17
|
+
plan/tasks/todos, document + scratchpad links and the materialized probe fired for every new draft.
|
|
18
|
+
- **One agent's name is one row.** `useAgentName` / `useAgentDescription` / `useAgentIdentity` ask
|
|
19
|
+
`fetchAgentRunTier(agentId)` when the record has not read the field; the whole catalogue is read only
|
|
20
|
+
when that fails. `fetchAgentRunTier` joins a run-tier read already in flight instead of forcing a second.
|
|
21
|
+
- **One mandate verdict per question.** The resolution GET is shared across the optional and required
|
|
22
|
+
lanes and with `resolveMandateHolder` (cached like resolutions; dropped on invalidation and org switch).
|
|
23
|
+
New `useMandateHolderSet(keys, { enabled })` — the verdict only; the composer's quick-action row uses it
|
|
24
|
+
instead of `useMandateSet`, so its jobs no longer read each definition + treatment row.
|
|
25
|
+
- **The greeting paints its final word.** `ComposerGreeting` ships an inline script that writes the local
|
|
26
|
+
time-of-day word before first paint; hydration renders the same word (no Largest Contentful Paint swap).
|
|
27
|
+
- The composer rail loads the saved context rules once the page is idle, not at mount.
|
|
28
|
+
- **Consumer action:** none.
|
|
29
|
+
|
|
3
30
|
## 0.7.20
|
|
4
31
|
|
|
5
32
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
6
33
|
`git diff npm/chat/v0.7.19..npm/chat/v0.7.20 -- apps/shared/chat`).
|
|
7
34
|
No source changes intended and no consumer action required.
|
|
8
35
|
|
|
36
|
+
## 0.7.19
|
|
37
|
+
|
|
38
|
+
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
39
|
+
`git diff npm/chat/v0.7.18..npm/chat/v0.7.19 -- apps/shared/chat`).
|
|
40
|
+
No source changes intended and no consumer action required.
|
|
41
|
+
|
|
9
42
|
## 0.7.18
|
|
10
43
|
|
|
11
44
|
Automatic changed-only republish (docs/metadata drift since the last tag — see
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { useAppDispatch, useAppSelector } from "../../../../store/hooks.js";
|
|
14
14
|
import { ensureSurfaceFeatureLoaded } from "../../../../surfaces/redux/userStateSlice.js";
|
|
15
15
|
import { CONTEXT_RULES_FEATURE } from "@ai-matrx/agents/context";
|
|
16
|
+
import { whenPageIdle } from "@ai-matrx/kit/idle-scheduler";
|
|
16
17
|
import { cn } from "@ai-matrx/design-system";
|
|
17
18
|
import { useIsMobile } from "@ai-matrx/kit/media-query";
|
|
18
19
|
import {
|
|
@@ -124,7 +125,11 @@ function ConversationContextRailBody({
|
|
|
124
125
|
useConversationFollowsPage(conversationId);
|
|
125
126
|
const contextChipShown = useConversationContextChipShown(conversationId) && withValueGroupChip;
|
|
126
127
|
useEffect(() => {
|
|
127
|
-
|
|
128
|
+
const idle = new AbortController();
|
|
129
|
+
void whenPageIdle(idle.signal).then((ready) => {
|
|
130
|
+
if (ready) void dispatch(ensureSurfaceFeatureLoaded(CONTEXT_RULES_FEATURE));
|
|
131
|
+
});
|
|
132
|
+
return () => idle.abort();
|
|
128
133
|
}, [dispatch]);
|
|
129
134
|
const workingDocEnabled = useAppSelector(
|
|
130
135
|
selectWorkingDocEnabled(conversationId, "working")
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* The list is DATA: the `agents.chat_composer.quick_actions` knob (system
|
|
7
7
|
* default, overridable per organization and per person). Each entry names a
|
|
8
8
|
* label and the agent JOB (mandate) it opens; the job resolves through the
|
|
9
|
-
* mandate ladder (`
|
|
9
|
+
* mandate ladder (`useMandateHolderSet`: the verdict only, never a launch read). An
|
|
10
10
|
* entry whose job cannot resolve renders disabled with the reason — never a
|
|
11
11
|
* silent fallback.
|
|
12
12
|
*/
|
|
@@ -10,7 +10,7 @@ import { ChevronRight } from "lucide-react";
|
|
|
10
10
|
import { isMandateKey } from "@ai-matrx/agents/mandates";
|
|
11
11
|
import { useAppSelector } from "../../../../../store/hooks.js";
|
|
12
12
|
import { selectActiveUserName } from "../../../../../host/identity.js";
|
|
13
|
-
import {
|
|
13
|
+
import { useMandateHolderSet } from "../../../../../mandates/useMandateHolderSet.js";
|
|
14
14
|
import { resolveMandate } from "../../../../../mandates/service.js";
|
|
15
15
|
import { toast } from "../../../../../host/notify.js";
|
|
16
16
|
import { useIdleReady } from "@ai-matrx/kit/idle-scheduler";
|
|
@@ -27,6 +27,7 @@ function timeOfDayGreeting(hour) {
|
|
|
27
27
|
}
|
|
28
28
|
const noSubscription = () => () => {
|
|
29
29
|
};
|
|
30
|
+
const GREETING_SCRIPT = `(function(s){var h=s&&s.previousElementSibling;var e=h&&h.querySelector('[data-matrx-greeting]');if(e)e.textContent=(${timeOfDayGreeting.toString()})(new Date().getHours())})(document.currentScript)`;
|
|
30
31
|
function ComposerGreeting({ className }) {
|
|
31
32
|
const userName = useAppSelector(selectActiveUserName);
|
|
32
33
|
const firstName = (userName ?? "").trim().split(/\s+/)[0] || "";
|
|
@@ -44,9 +45,10 @@ function ComposerGreeting({ className }) {
|
|
|
44
45
|
}
|
|
45
46
|
),
|
|
46
47
|
/* @__PURE__ */ jsxs("h1", { className: "font-serif text-[clamp(2rem,1.6rem+1.8vw,2.875rem)] font-normal tracking-tight text-foreground", children: [
|
|
47
|
-
greeting ?? "Hello",
|
|
48
|
+
/* @__PURE__ */ jsx("span", { "data-matrx-greeting": "", suppressHydrationWarning: true, children: greeting ?? "Hello" }),
|
|
48
49
|
firstName ? `, ${firstName}` : ""
|
|
49
|
-
] })
|
|
50
|
+
] }),
|
|
51
|
+
/* @__PURE__ */ jsx("script", { dangerouslySetInnerHTML: { __html: GREETING_SCRIPT } })
|
|
50
52
|
] });
|
|
51
53
|
}
|
|
52
54
|
function parseQuickActions(value) {
|
|
@@ -77,12 +79,12 @@ function ComposerQuickActions({
|
|
|
77
79
|
const actions = parseQuickActions(knob);
|
|
78
80
|
const quickActionKeys = actions.map((a) => a.mandateKey);
|
|
79
81
|
const idle = useIdleReady();
|
|
80
|
-
const mandates =
|
|
82
|
+
const mandates = useMandateHolderSet(quickActionKeys, {
|
|
81
83
|
enabled: Boolean(organizationId) && idle
|
|
82
84
|
});
|
|
83
85
|
const [launching, setLaunching] = useState(null);
|
|
84
86
|
const launch = (action) => {
|
|
85
|
-
const known = mandates[action.mandateKey]?.
|
|
87
|
+
const known = mandates[action.mandateKey]?.agentId;
|
|
86
88
|
if (known) {
|
|
87
89
|
onLaunchAgent(known);
|
|
88
90
|
return;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { getChatHost } from "../../host/configure.js";
|
|
3
3
|
const getAgentCatalog = () => getChatHost().catalog();
|
|
4
|
-
import { useAppSelector } from "../../store/hooks.js";
|
|
5
|
-
import { useEffect, useMemo, useSyncExternalStore } from "react";
|
|
4
|
+
import { useAppDispatch, useAppSelector } from "../../store/hooks.js";
|
|
5
|
+
import { useEffect, useMemo, useState, useSyncExternalStore } from "react";
|
|
6
|
+
import { fetchAgentRunTier } from "../redux/agent-definition/thunks.js";
|
|
6
7
|
import { hasField } from "@ai-matrx/agents/field-flags";
|
|
7
8
|
import { selectAgentById } from "../redux/agent-definition/selectors.js";
|
|
8
9
|
import { makeEmptyRecord } from "../redux/agent-definition/slice.js";
|
|
@@ -114,21 +115,40 @@ function useAgentView(agentId) {
|
|
|
114
115
|
function readAgentView(state, agentId) {
|
|
115
116
|
return agentViewOf(recordOf(state, agentId), catalogRow(agentId));
|
|
116
117
|
}
|
|
118
|
+
function useIdentityRow(agentId, record, fields) {
|
|
119
|
+
const dispatch = useAppDispatch();
|
|
120
|
+
const needed = Boolean(agentId) && catalogNeededFor(record, fields);
|
|
121
|
+
const [runTierFailedFor, setRunTierFailedFor] = useState(null);
|
|
122
|
+
useEffect(() => {
|
|
123
|
+
if (!agentId || !needed) return void 0;
|
|
124
|
+
let cancelled = false;
|
|
125
|
+
dispatch(fetchAgentRunTier(agentId)).unwrap().catch(() => {
|
|
126
|
+
if (!cancelled) setRunTierFailedFor(agentId);
|
|
127
|
+
});
|
|
128
|
+
return () => {
|
|
129
|
+
cancelled = true;
|
|
130
|
+
};
|
|
131
|
+
}, [dispatch, agentId, needed]);
|
|
132
|
+
return useAgentSummaryRow(agentId, { load: needed && runTierFailedFor === agentId });
|
|
133
|
+
}
|
|
117
134
|
function useAgentName(agentId) {
|
|
118
135
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
119
|
-
const row =
|
|
136
|
+
const row = useIdentityRow(agentId, record, NAME);
|
|
120
137
|
return nameOf(record, row);
|
|
121
138
|
}
|
|
122
139
|
function useAgentDescription(agentId) {
|
|
123
140
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
124
|
-
const row =
|
|
141
|
+
const row = useIdentityRow(agentId, record, DESCRIPTION);
|
|
125
142
|
return descriptionOf(record, row);
|
|
126
143
|
}
|
|
127
144
|
function useAgentIdentity(agentId) {
|
|
128
145
|
const record = useAppSelector((s) => recordOf(s, agentId));
|
|
129
|
-
const summary =
|
|
146
|
+
const summary = useIdentityRow(agentId, record, NAME_AND_DESCRIPTION);
|
|
130
147
|
return { name: nameOf(record, summary), description: descriptionOf(record, summary), summary };
|
|
131
148
|
}
|
|
149
|
+
const NAME = ["name"];
|
|
150
|
+
const DESCRIPTION = ["description"];
|
|
151
|
+
const NAME_AND_DESCRIPTION = ["name", "description"];
|
|
132
152
|
function readAgentName(state, agentId) {
|
|
133
153
|
return nameOf(recordOf(state, agentId), catalogRow(agentId));
|
|
134
154
|
}
|
|
@@ -140,7 +140,9 @@ const fetchAgentRunTier = createAsyncThunk(
|
|
|
140
140
|
if (await isSignedOutVisitor()) throw new NotAuthenticatedError();
|
|
141
141
|
dispatch(setAgentLoading({ id: agentId, loading: true }));
|
|
142
142
|
try {
|
|
143
|
-
|
|
143
|
+
const store = getRunTierStore();
|
|
144
|
+
const inFlight = store.getState().runTier[agentId]?.status === "loading";
|
|
145
|
+
await store.loadRunTier(agentId, { force: !inFlight });
|
|
144
146
|
} catch (e) {
|
|
145
147
|
dispatch(setAgentLoading({ id: agentId, loading: false }));
|
|
146
148
|
const failure = runTierFailure(agentId, e);
|
|
@@ -270,10 +270,15 @@ export declare const selectIsCacheOnly: (conversationId: string) => (state: Chat
|
|
|
270
270
|
* Is this a DRAFT — a conversation minted in this tab that has no server row
|
|
271
271
|
* yet (`cacheOnly`, until the first send's confirmation or a DB hydrate flips
|
|
272
272
|
* it)? Reads keyed by a draft's id can only answer "nothing": gate them on
|
|
273
|
-
* this and they run the moment the first send makes it real.
|
|
274
|
-
*
|
|
275
|
-
*
|
|
276
|
-
*
|
|
273
|
+
* this and they run the moment the first send makes it real.
|
|
274
|
+
*
|
|
275
|
+
* A conversation Redux holds answers from its record. One Redux does NOT hold
|
|
276
|
+
* is a draft only if this tab minted its id (`wasMintedInThisTab`) — the
|
|
277
|
+
* window between the launcher minting the id and the launch thunk creating the
|
|
278
|
+
* record, in which /chat/new's composer has already mounted and run its
|
|
279
|
+
* effects. Any other unknown id was opened from elsewhere, so it has a row —
|
|
280
|
+
* unlike `selectIsCacheOnly`, which answers true for every unknown id because
|
|
281
|
+
* it gates WRITES.
|
|
277
282
|
*/
|
|
278
283
|
export declare const selectIsDraftConversation: (conversationId: string | null | undefined) => (state: ChatRootState) => boolean;
|
|
279
284
|
export declare const selectConversationTitle: (conversationId: string) => (state: ChatRootState) => string | null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createSelector } from "@reduxjs/toolkit";
|
|
2
|
+
import { wasMintedInThisTab } from "../utils/ids.js";
|
|
2
3
|
const EMPTY_INSTANCES = [];
|
|
3
4
|
const EMPTY_CONVERSATION_IDS_BY_AGENT = {};
|
|
4
5
|
const selectInstance = (conversationId) => (state) => state.conversations.byConversationId[conversationId];
|
|
@@ -44,8 +45,12 @@ const selectRunningInstances = createSelector(
|
|
|
44
45
|
);
|
|
45
46
|
const selectAgentIdFromInstance = (conversationId) => (state) => state.conversations.byConversationId[conversationId]?.agentId;
|
|
46
47
|
const selectIsCacheOnly = (conversationId) => (state) => state.conversations.byConversationId[conversationId]?.cacheOnly ?? true;
|
|
47
|
-
const selectIsDraftConversation = (conversationId) => (state) =>
|
|
48
|
-
|
|
48
|
+
const selectIsDraftConversation = (conversationId) => (state) => {
|
|
49
|
+
if (!conversationId) return false;
|
|
50
|
+
const record = state.conversations?.byConversationId[conversationId];
|
|
51
|
+
if (record) return record.cacheOnly === true;
|
|
52
|
+
return wasMintedInThisTab(conversationId);
|
|
53
|
+
};
|
|
49
54
|
const selectConversationTitle = (conversationId) => (state) => state.conversations.byConversationId[conversationId]?.title ?? null;
|
|
50
55
|
const selectConversationDescription = (conversationId) => (state) => state.conversations.byConversationId[conversationId]?.description ?? null;
|
|
51
56
|
const selectConversationKeywords = (conversationId) => (state) => state.conversations.byConversationId[conversationId]?.keywords ?? null;
|
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
* as-is and used as the authoritative conversation key.
|
|
7
7
|
*/
|
|
8
8
|
export declare const generateConversationId: () => string;
|
|
9
|
+
/** True for an id `generateConversationId` minted in this tab. */
|
|
10
|
+
export declare const wasMintedInThisTab: (conversationId: string) => boolean;
|
|
9
11
|
/** @deprecated Use generateConversationId() */
|
|
10
12
|
export declare const generateInstanceId: typeof generateConversationId;
|
|
11
13
|
export declare const generateResourceId: () => string;
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
const
|
|
1
|
+
const mintedInThisTab = /* @__PURE__ */ new Set();
|
|
2
|
+
const generateConversationId = () => {
|
|
3
|
+
const id = crypto.randomUUID();
|
|
4
|
+
mintedInThisTab.add(id);
|
|
5
|
+
return id;
|
|
6
|
+
};
|
|
7
|
+
const wasMintedInThisTab = (conversationId) => mintedInThisTab.has(conversationId);
|
|
2
8
|
const generateInstanceId = generateConversationId;
|
|
3
9
|
const generateResourceId = () => `res_${crypto.randomUUID()}`;
|
|
4
10
|
const generateRequestId = () => `req_${crypto.randomUUID()}`;
|
|
@@ -8,5 +14,6 @@ export {
|
|
|
8
14
|
generateInstanceId,
|
|
9
15
|
generateRequestId,
|
|
10
16
|
generateResourceId,
|
|
11
|
-
generateScopeId
|
|
17
|
+
generateScopeId,
|
|
18
|
+
wasMintedInThisTab
|
|
12
19
|
};
|
package/dist/mandates/service.js
CHANGED
|
@@ -96,7 +96,12 @@ function dropMandateCacheForOrgSwitch() {
|
|
|
96
96
|
cache.delete(key);
|
|
97
97
|
dropped = true;
|
|
98
98
|
}
|
|
99
|
+
for (const key of verdictCache.keys()) {
|
|
100
|
+
if (current !== null && key.split(":")[1] === current) continue;
|
|
101
|
+
verdictCache.delete(key);
|
|
102
|
+
}
|
|
99
103
|
resolutionsInFlight.clear();
|
|
104
|
+
verdictsInFlight.clear();
|
|
100
105
|
cacheGeneration += 1;
|
|
101
106
|
if (!dropped) return;
|
|
102
107
|
for (const listener of invalidationListeners) listener(void 0);
|
|
@@ -111,13 +116,18 @@ function invalidateMandateCache(mandateKey) {
|
|
|
111
116
|
invalidateMandateCatalogueCache();
|
|
112
117
|
cacheGeneration += 1;
|
|
113
118
|
resolutionsInFlight.clear();
|
|
119
|
+
verdictsInFlight.clear();
|
|
114
120
|
if (mandateKey) {
|
|
115
121
|
for (const key of cache.keys()) {
|
|
116
122
|
if (key.endsWith(`:${mandateKey}`)) cache.delete(key);
|
|
117
123
|
}
|
|
124
|
+
for (const key of verdictCache.keys()) {
|
|
125
|
+
if (key.endsWith(`:${mandateKey}`)) verdictCache.delete(key);
|
|
126
|
+
}
|
|
118
127
|
pinCache.delete(mandateKey);
|
|
119
128
|
} else {
|
|
120
129
|
cache.clear();
|
|
130
|
+
verdictCache.clear();
|
|
121
131
|
pinCache.clear();
|
|
122
132
|
}
|
|
123
133
|
for (const listener of invalidationListeners) listener(mandateKey);
|
|
@@ -172,7 +182,7 @@ async function selectedOrganizationFor(mandateKey) {
|
|
|
172
182
|
}
|
|
173
183
|
async function fetchResolutionVerdict(mandateKey, options) {
|
|
174
184
|
const question = await resolutionQuestion(mandateKey, options);
|
|
175
|
-
const verdict = await
|
|
185
|
+
const verdict = await askVerdict(mandateKey, options, question);
|
|
176
186
|
if (verdict === null) return null;
|
|
177
187
|
return { kind: "verdict", verdict, ...question };
|
|
178
188
|
}
|
|
@@ -189,7 +199,7 @@ async function resolutionQuestion(mandateKey, options) {
|
|
|
189
199
|
const organizationId = options.organizationId ?? await selectedOrganizationFor(mandateKey);
|
|
190
200
|
return { organizationId, cacheKey: mandateCacheKey(userId, organizationId, mandateKey), guest: false };
|
|
191
201
|
}
|
|
192
|
-
async function askResolutionDoor(mandateKey,
|
|
202
|
+
async function askResolutionDoor(mandateKey, captureErrors, organizationId) {
|
|
193
203
|
let verdict;
|
|
194
204
|
try {
|
|
195
205
|
const { data } = await apiGet(
|
|
@@ -210,43 +220,78 @@ async function askResolutionDoor(mandateKey, options, organizationId) {
|
|
|
210
220
|
// answering 404 is the documented result, not a system error, so it must
|
|
211
221
|
// not enter the global Error Inspector. Anything that is not a 404 is
|
|
212
222
|
// re-thrown below and captured by the consumer that asked.
|
|
213
|
-
captureErrors
|
|
223
|
+
captureErrors
|
|
214
224
|
}
|
|
215
225
|
);
|
|
216
226
|
verdict = data;
|
|
217
227
|
} catch (error) {
|
|
218
|
-
if (error instanceof BackendApiError && error.status === 404)
|
|
219
|
-
if (options.optional) return null;
|
|
220
|
-
throw recordUnavailable({
|
|
221
|
-
entity: "mandate",
|
|
222
|
-
reason: "unknown",
|
|
223
|
-
recordId: mandateKey,
|
|
224
|
-
relation: "mandate.definition"
|
|
225
|
-
});
|
|
226
|
-
}
|
|
228
|
+
if (error instanceof BackendApiError && error.status === 404) return null;
|
|
227
229
|
throw error;
|
|
228
230
|
}
|
|
229
231
|
return verdict;
|
|
230
232
|
}
|
|
233
|
+
function mandateMissing(mandateKey) {
|
|
234
|
+
return recordUnavailable({
|
|
235
|
+
entity: "mandate",
|
|
236
|
+
reason: "unknown",
|
|
237
|
+
recordId: mandateKey,
|
|
238
|
+
relation: "mandate.definition"
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
async function askVerdict(mandateKey, options, question) {
|
|
242
|
+
const answer = await sharedVerdict(mandateKey, options, question);
|
|
243
|
+
if (answer === MISSING) {
|
|
244
|
+
if (options.optional) return null;
|
|
245
|
+
throw mandateMissing(mandateKey);
|
|
246
|
+
}
|
|
247
|
+
return answer;
|
|
248
|
+
}
|
|
249
|
+
const MISSING = "missing";
|
|
250
|
+
const verdictCache = /* @__PURE__ */ new Map();
|
|
251
|
+
const verdictsInFlight = /* @__PURE__ */ new Map();
|
|
252
|
+
function sharedVerdict(mandateKey, options, question) {
|
|
253
|
+
const key = question.cacheKey;
|
|
254
|
+
const cached = verdictCache.get(key);
|
|
255
|
+
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return Promise.resolve(cached.value);
|
|
256
|
+
const pending = verdictsInFlight.get(key);
|
|
257
|
+
if (pending) return pending;
|
|
258
|
+
const generation = cacheGeneration;
|
|
259
|
+
const run = askResolutionDoor(mandateKey, !options.optional, question.organizationId).then((verdict) => {
|
|
260
|
+
const value = verdict ?? MISSING;
|
|
261
|
+
if (generation === cacheGeneration) verdictCache.set(key, { at: Date.now(), value });
|
|
262
|
+
return value;
|
|
263
|
+
}).finally(() => {
|
|
264
|
+
if (verdictsInFlight.get(key) === run) verdictsInFlight.delete(key);
|
|
265
|
+
});
|
|
266
|
+
verdictsInFlight.set(key, run);
|
|
267
|
+
return run;
|
|
268
|
+
}
|
|
231
269
|
async function resolveMandate(mandateKey, options = {}) {
|
|
232
270
|
const question = await resolutionQuestion(mandateKey, options);
|
|
233
271
|
const cached = cache.get(question.cacheKey);
|
|
234
272
|
if (cached && Date.now() - cached.at < CACHE_TTL_MS) return cached.value;
|
|
235
|
-
const flightKey =
|
|
236
|
-
|
|
237
|
-
if (
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
273
|
+
const flightKey = question.cacheKey;
|
|
274
|
+
let run = resolutionsInFlight.get(flightKey);
|
|
275
|
+
if (!run) {
|
|
276
|
+
const started = resolveFromDoor(mandateKey, options, question).finally(() => {
|
|
277
|
+
if (resolutionsInFlight.get(flightKey) === started) resolutionsInFlight.delete(flightKey);
|
|
278
|
+
});
|
|
279
|
+
resolutionsInFlight.set(flightKey, started);
|
|
280
|
+
run = started;
|
|
281
|
+
}
|
|
282
|
+
const answer = await run;
|
|
283
|
+
if (answer === MISSING) {
|
|
284
|
+
if (options.optional) return null;
|
|
285
|
+
throw mandateMissing(mandateKey);
|
|
286
|
+
}
|
|
287
|
+
return answer;
|
|
243
288
|
}
|
|
244
289
|
const resolutionsInFlight = /* @__PURE__ */ new Map();
|
|
245
290
|
async function resolveFromDoor(mandateKey, options, question) {
|
|
246
291
|
const supabase = createClient();
|
|
247
292
|
const generation = cacheGeneration;
|
|
248
|
-
const verdict = await
|
|
249
|
-
if (verdict ===
|
|
293
|
+
const verdict = await sharedVerdict(mandateKey, options, question);
|
|
294
|
+
if (verdict === MISSING) return MISSING;
|
|
250
295
|
const { organizationId, cacheKey, guest } = question;
|
|
251
296
|
const holder = assertRunnableVerdict(mandateKey, verdict);
|
|
252
297
|
const provenance = verdict.provenance;
|
|
@@ -283,15 +328,7 @@ async function resolveFromDoor(mandateKey, options, question) {
|
|
|
283
328
|
}
|
|
284
329
|
const { data: mandate, error } = await mandateDefinitions(supabase).select("*").eq("mandate_key", mandateKey).is("deleted_at", null).maybeSingle();
|
|
285
330
|
if (error) throw error;
|
|
286
|
-
if (!mandate)
|
|
287
|
-
if (options.optional) return null;
|
|
288
|
-
throw recordUnavailable({
|
|
289
|
-
entity: "mandate",
|
|
290
|
-
reason: "unknown",
|
|
291
|
-
recordId: mandateKey,
|
|
292
|
-
relation: "mandate.definition"
|
|
293
|
-
});
|
|
294
|
-
}
|
|
331
|
+
if (!mandate) return MISSING;
|
|
295
332
|
let presentation = null;
|
|
296
333
|
{
|
|
297
334
|
const { data: treatment, error: treatmentError } = await mandateTreatments(
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { AnyMandateKey } from "@ai-matrx/agents/mandates";
|
|
2
|
+
export interface MandateHolderState {
|
|
3
|
+
agentId: string | null;
|
|
4
|
+
loading: boolean;
|
|
5
|
+
error: string | null;
|
|
6
|
+
}
|
|
7
|
+
export declare function useMandateHolderSet(keys: readonly AnyMandateKey[], options?: {
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
}): Readonly<Record<string, MandateHolderState>>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { useEffect, useState } from "react";
|
|
3
|
+
import { extractErrorMessage } from "@ai-matrx/data/net";
|
|
4
|
+
import { onMandateCacheInvalidated, resolveMandateHolder } from "./service.js";
|
|
5
|
+
import { useAppSelector } from "../store/hooks.js";
|
|
6
|
+
import { selectOrganizationId } from "../host/org.js";
|
|
7
|
+
const SEPARATOR = "\0";
|
|
8
|
+
function useMandateHolderSet(keys, options = {}) {
|
|
9
|
+
const enabled = options.enabled ?? true;
|
|
10
|
+
const organizationId = useAppSelector(selectOrganizationId) ?? null;
|
|
11
|
+
const keyList = enabled ? keys.join(SEPARATOR) : "";
|
|
12
|
+
const question = keyList ? `${keyList}|${organizationId ?? ""}` : "";
|
|
13
|
+
const [state, setState] = useState(
|
|
14
|
+
() => ({ question: "", epoch: 0, set: {} })
|
|
15
|
+
);
|
|
16
|
+
if (state.question !== question) setState({ question, epoch: 0, set: {} });
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!keyList) return void 0;
|
|
19
|
+
const listed = keyList.split(SEPARATOR);
|
|
20
|
+
return onMandateCacheInvalidated((key) => {
|
|
21
|
+
if (key === void 0 || listed.includes(key)) setState((prev) => ({ ...prev, epoch: prev.epoch + 1 }));
|
|
22
|
+
});
|
|
23
|
+
}, [keyList]);
|
|
24
|
+
const epoch = state.epoch;
|
|
25
|
+
useEffect(() => {
|
|
26
|
+
if (!keyList) return void 0;
|
|
27
|
+
const listed = keyList.split(SEPARATOR);
|
|
28
|
+
let cancelled = false;
|
|
29
|
+
void Promise.allSettled(listed.map((key) => resolveMandateHolder(key))).then((results) => {
|
|
30
|
+
if (cancelled) return;
|
|
31
|
+
const next = {};
|
|
32
|
+
results.forEach((result, i) => {
|
|
33
|
+
const key = listed[i];
|
|
34
|
+
if (result.status === "fulfilled" && result.value?.holderType === "agent") {
|
|
35
|
+
next[key] = { agentId: result.value.holderId, loading: false, error: null };
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
const message = result.status === "rejected" ? extractErrorMessage(result.reason) : `${key} is held by a workflow, not an agent`;
|
|
39
|
+
console.error(`[mandates] ${key} has no agent to open:`, message);
|
|
40
|
+
next[key] = { agentId: null, loading: false, error: message };
|
|
41
|
+
});
|
|
42
|
+
setState((prev) => prev.question === question ? { ...prev, set: next } : prev);
|
|
43
|
+
});
|
|
44
|
+
return () => {
|
|
45
|
+
cancelled = true;
|
|
46
|
+
};
|
|
47
|
+
}, [keyList, question, epoch]);
|
|
48
|
+
return state.set;
|
|
49
|
+
}
|
|
50
|
+
export {
|
|
51
|
+
useMandateHolderSet
|
|
52
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-matrx/chat",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.22",
|
|
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 explicit per-module subpaths @ai-matrx/chat/<domain>/<module> (no wildcards: scripts/public-modules.json) over a host contract (/host, /host/react) whose only required value is a db. One chat, one renderer: the old kernel entries (., /external-store, /matrx, /react, /tools, styles.css, tokens.css) are retired.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -4681,25 +4681,25 @@
|
|
|
4681
4681
|
"server-only": "^0.0.1",
|
|
4682
4682
|
"uuid": "^14.0.2",
|
|
4683
4683
|
"zod": "^4.6.5",
|
|
4684
|
-
"@ai-matrx/alchemy": "latest",
|
|
4685
|
-
"@ai-matrx/associations": "latest",
|
|
4686
4684
|
"@ai-matrx/browser-audio": "latest",
|
|
4687
4685
|
"@ai-matrx/canvas": "latest",
|
|
4686
|
+
"@ai-matrx/associations": "latest",
|
|
4688
4687
|
"@ai-matrx/content-ir": "latest",
|
|
4689
4688
|
"@ai-matrx/agents": "latest",
|
|
4690
|
-
"@ai-matrx/
|
|
4691
|
-
"@ai-matrx/data": "latest",
|
|
4689
|
+
"@ai-matrx/alchemy": "latest",
|
|
4692
4690
|
"@ai-matrx/design-system": "latest",
|
|
4693
|
-
"@ai-matrx/diff": "latest",
|
|
4694
|
-
"@ai-matrx/icons": "latest",
|
|
4695
4691
|
"@ai-matrx/kit": "latest",
|
|
4696
4692
|
"@ai-matrx/media": "latest",
|
|
4693
|
+
"@ai-matrx/print": "latest",
|
|
4694
|
+
"@ai-matrx/diff": "latest",
|
|
4697
4695
|
"@ai-matrx/realtime": "latest",
|
|
4698
4696
|
"@ai-matrx/records": "latest",
|
|
4699
4697
|
"@ai-matrx/records-ui": "latest",
|
|
4700
|
-
"@ai-matrx/print": "latest",
|
|
4701
4698
|
"@ai-matrx/rich-content": "latest",
|
|
4702
|
-
"@ai-matrx/
|
|
4699
|
+
"@ai-matrx/data": "latest",
|
|
4700
|
+
"@ai-matrx/rich-editor": "latest",
|
|
4701
|
+
"@ai-matrx/content-ir-react": "latest",
|
|
4702
|
+
"@ai-matrx/icons": "latest"
|
|
4703
4703
|
},
|
|
4704
4704
|
"devDependencies": {
|
|
4705
4705
|
"@arethetypeswrong/cli": "^0.18.5",
|
|
@@ -4818,5 +4818,5 @@
|
|
|
4818
4818
|
"check:side-effects": "node ../scripts/check-side-effects-listed.mjs . && node ../scripts/check-side-effects-listed.mjs --self-test",
|
|
4819
4819
|
"check:public-surface": "node ./scripts/sync-public-surface.mjs --check && node ./scripts/sync-public-surface.mjs --self-test"
|
|
4820
4820
|
},
|
|
4821
|
-
"gitHead": "
|
|
4821
|
+
"gitHead": "ec969cf6169fd1c376feb4eb7e4ec3e5aa07e607"
|
|
4822
4822
|
}
|