@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3
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/README.md +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +24441 -14433
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +16 -2
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +38 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +84 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +56 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +191 -74
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -50
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +23 -153
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -92
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +24183 -17182
- package/src/generic/FireAndForgetHeartbeat.ts +16 -2
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +97 -1
- package/src/generic/ResolverBase.ts +220 -76
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +228 -51
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
- package/src/resolvers/RunAIAgentResolver.ts +28 -174
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
28
28
|
import { AppContext, UserPayload } from '../types.js';
|
|
29
|
-
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView } from '@memberjunction/core';
|
|
29
|
+
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
|
|
30
30
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
31
31
|
import {
|
|
32
32
|
MJAIAgentEntity,
|
|
@@ -48,6 +48,8 @@ import {
|
|
|
48
48
|
FindIgnoredRealtimeConfigKeys,
|
|
49
49
|
ParseRealtimeTypeConfiguration,
|
|
50
50
|
ResolveEffectiveRealtimeConfig,
|
|
51
|
+
ResolveRealtimeCoAgentID,
|
|
52
|
+
FilterAllowedAgentsByCanRun,
|
|
51
53
|
RealtimeAllowedAgent,
|
|
52
54
|
RealtimeDirectActionsConfig,
|
|
53
55
|
REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION,
|
|
@@ -56,7 +58,7 @@ import {
|
|
|
56
58
|
writeRealtimeRecordingSegment,
|
|
57
59
|
deleteRealtimeRecordingSegments,
|
|
58
60
|
} from '@memberjunction/ai-agents';
|
|
59
|
-
import { AgentExecutionProgressCallback,
|
|
61
|
+
import { AgentExecutionProgressCallback, AppContextSnapshot } from '@memberjunction/ai-core-plus';
|
|
60
62
|
import { ChatMessage, RealtimeToolDefinition } from '@memberjunction/ai';
|
|
61
63
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
62
64
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
@@ -71,32 +73,6 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
|
|
|
71
73
|
*/
|
|
72
74
|
const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
73
75
|
|
|
74
|
-
/**
|
|
75
|
-
* The seeded name of the internal orchestration agent that fronts a target agent in realtime
|
|
76
|
-
* sessions (voice + interactive channels). This is the GLOBAL DEFAULT co-agent — the final step of
|
|
77
|
-
* the co-agent resolution chain (see {@link RealtimeClientSessionResolver.resolveCoAgentID}).
|
|
78
|
-
* Deployments can override it per agent (`AIAgent.DefaultCoAgentID`), per agent type (an
|
|
79
|
-
* `AIAgentCoAgent` row with `TargetAgentTypeID` + `IsDefault`), or per call (the `coAgentId`
|
|
80
|
-
* mutation argument) without
|
|
81
|
-
* touching this seed.
|
|
82
|
-
*/
|
|
83
|
-
const REALTIME_CO_AGENT_NAME = 'Realtime Co-Agent';
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* DEPRECATED legacy seed name of {@link REALTIME_CO_AGENT_NAME}, from before the agent's rename
|
|
87
|
-
* from "Voice Co-Agent" to "Realtime Co-Agent". Deployments that have not re-synced the agent seed
|
|
88
|
-
* still carry this name, so {@link RealtimeClientSessionResolver.resolveGlobalCoAgentID} falls
|
|
89
|
-
* back to it (with a deprecation log) when no agent named {@link REALTIME_CO_AGENT_NAME} exists.
|
|
90
|
-
*/
|
|
91
|
-
const LEGACY_REALTIME_CO_AGENT_NAME = 'Voice Co-Agent';
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* The seeded name of the Realtime agent TYPE. Every co-agent candidate (explicit or metadata
|
|
95
|
-
* default) must be an Active agent of this type — a co-agent drives a streaming full-duplex
|
|
96
|
-
* realtime session, which only `RealtimeAgentType`-driven agents support.
|
|
97
|
-
*/
|
|
98
|
-
const REALTIME_AGENT_TYPE_NAME = 'Realtime';
|
|
99
|
-
|
|
100
76
|
/** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
101
77
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
102
78
|
const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
|
|
@@ -381,6 +357,31 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
381
357
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
382
358
|
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
383
359
|
|
|
360
|
+
/**
|
|
361
|
+
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
362
|
+
* `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
|
|
363
|
+
* runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
|
|
364
|
+
* When a subclass refused the write without recording anything, say so explicitly instead of
|
|
365
|
+
* "unknown error", which is what hid MJ#4791.
|
|
366
|
+
*
|
|
367
|
+
* Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
|
|
368
|
+
* registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
|
|
369
|
+
* refusal registered nothing — so its text must never be reported as this failure's reason.
|
|
370
|
+
*/
|
|
371
|
+
private describeSaveFailure(entity: BaseEntity): string {
|
|
372
|
+
const latest = entity.LatestResult;
|
|
373
|
+
if (!latest) {
|
|
374
|
+
return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
|
|
375
|
+
'— the write was refused before reaching the provider';
|
|
376
|
+
}
|
|
377
|
+
if (latest.Success) {
|
|
378
|
+
return 'no failure detail recorded (latest result entry is a prior success; ' +
|
|
379
|
+
`ResultHistory length ${entity.ResultHistory.length})`;
|
|
380
|
+
}
|
|
381
|
+
const detail = latest.CompleteMessage?.trim();
|
|
382
|
+
return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
|
|
383
|
+
}
|
|
384
|
+
|
|
384
385
|
/**
|
|
385
386
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
386
387
|
*
|
|
@@ -389,7 +390,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
389
390
|
* session is created.
|
|
390
391
|
* 2. Resolve the co-agent id via the metadata-driven resolution chain (runtime `coAgentId` →
|
|
391
392
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
392
|
-
* see {@link
|
|
393
|
+
* see {@link ResolveRealtimeCoAgentID} for the full contract.
|
|
393
394
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
394
395
|
* config server-side — this is the authoritative target for all later relays.
|
|
395
396
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
@@ -438,7 +439,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
438
439
|
): Promise<StartRealtimeClientSessionResult> {
|
|
439
440
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
440
441
|
|
|
441
|
-
const coAgentID = await
|
|
442
|
+
const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
|
|
442
443
|
// PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
|
|
443
444
|
// (with the IsDefault row standing in when no runtime target was supplied); zero rows =
|
|
444
445
|
// universal, today's behavior untouched. Resolves the AUTHORITATIVE target id.
|
|
@@ -553,7 +554,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
553
554
|
TargetAgentID: config.targetAgentID,
|
|
554
555
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
555
556
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
556
|
-
AllowedAgents: await
|
|
557
|
+
AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
557
558
|
DirectActions: config.directActions,
|
|
558
559
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
559
560
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
@@ -593,7 +594,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
593
594
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
594
595
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
595
596
|
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
596
|
-
|
|
597
|
+
// The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
|
|
598
|
+
// — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
|
|
599
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
|
|
597
600
|
|
|
598
601
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
599
602
|
return ResultJson;
|
|
@@ -657,7 +660,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
657
660
|
session.Config_ = JSON.stringify(next);
|
|
658
661
|
if (!(await session.Save())) {
|
|
659
662
|
LogError(
|
|
660
|
-
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed
|
|
663
|
+
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
661
664
|
);
|
|
662
665
|
}
|
|
663
666
|
}
|
|
@@ -1223,39 +1226,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1223
1226
|
}
|
|
1224
1227
|
}
|
|
1225
1228
|
|
|
1226
|
-
/**
|
|
1227
|
-
* Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
|
|
1228
|
-
*
|
|
1229
|
-
* {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
|
|
1230
|
-
* with was never gated at all — a model-named colleague resolves straight to a delegated run.
|
|
1231
|
-
* That was survivable while the run carried the caller's own identity, because base-agent
|
|
1232
|
-
* re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
|
|
1233
|
-
* caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
|
|
1234
|
-
* caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
|
|
1235
|
-
* or not — the authorization identity must never depend on the elevation decision.
|
|
1236
|
-
*
|
|
1237
|
-
* `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
|
|
1238
|
-
* closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
|
|
1239
|
-
* A filtered-out colleague is not an error: the delegation layer reports it as "not available in
|
|
1240
|
-
* this session" and lists what remains, which is the same answer the model gets for a typo.
|
|
1241
|
-
*
|
|
1242
|
-
* @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
|
|
1243
|
-
* @param contextUser The ORIGINAL caller — never the elevated run user.
|
|
1244
|
-
* @returns The subset the caller may run, preserving order.
|
|
1245
|
-
*/
|
|
1246
|
-
private async filterAllowedAgentsByCanRun(
|
|
1247
|
-
allowedAgents: RealtimeAllowedAgent[] | undefined,
|
|
1248
|
-
contextUser: UserInfo,
|
|
1249
|
-
): Promise<RealtimeAllowedAgent[] | undefined> {
|
|
1250
|
-
if (!allowedAgents || allowedAgents.length === 0) {
|
|
1251
|
-
return allowedAgents;
|
|
1252
|
-
}
|
|
1253
|
-
const verdicts = await Promise.all(
|
|
1254
|
-
allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')),
|
|
1255
|
-
);
|
|
1256
|
-
return allowedAgents.filter((_, i) => verdicts[i]);
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
1229
|
/**
|
|
1260
1230
|
* Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
|
|
1261
1231
|
* (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
|
|
@@ -1498,195 +1468,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1498
1468
|
}
|
|
1499
1469
|
}
|
|
1500
1470
|
|
|
1501
|
-
/**
|
|
1502
|
-
* Resolves the co-agent (the Realtime-type agent that voices the target agent) for a new
|
|
1503
|
-
* client-direct session via the metadata-driven **CO-AGENT RESOLUTION CHAIN** — first match
|
|
1504
|
-
* wins, evaluated in precedence order:
|
|
1505
|
-
*
|
|
1506
|
-
* 1. **Runtime parameter** — the mutation's explicit `coAgentId`. A per-call override; an
|
|
1507
|
-
* invalid candidate (unknown, not Active, or not of the Realtime agent type) **throws**
|
|
1508
|
-
* (fail loud — the caller asked for something specific, mirroring `preferredModelId`).
|
|
1509
|
-
* 2. **Per-agent persona** — the target agent's `AIAgent.DefaultCoAgentID`. An invalid
|
|
1510
|
-
* reference logs a warning and **falls through** to the next step (stale metadata should
|
|
1511
|
-
* degrade gracefully, never break calls).
|
|
1512
|
-
* 3. **Per-type default** — an Active `AIAgentCoAgent` row of Type `'CoAgent'` whose
|
|
1513
|
-
* `TargetAgentTypeID` is the target agent's type and `IsDefault = 1` (lowest `Sequence`
|
|
1514
|
-
* wins a tie). Same tolerant warn-and-fall-through semantics as step 2.
|
|
1515
|
-
* 4. **Global default** — the seeded {@link REALTIME_CO_AGENT_NAME} agent, looked up by name
|
|
1516
|
-
* (with a deprecated fallback to {@link LEGACY_REALTIME_CO_AGENT_NAME}). Throws when absent
|
|
1517
|
-
* entirely (the realtime feature is unconfigured in this deployment).
|
|
1518
|
-
*
|
|
1519
|
-
* Every candidate from steps 1–3 is validated by {@link findValidCoAgent}: it must exist in
|
|
1520
|
-
* {@link AIEngine}'s cached agents, have Status `Active`, and be of the
|
|
1521
|
-
* {@link REALTIME_AGENT_TYPE_NAME} agent type. Step 4 keeps its original name-lookup contract.
|
|
1522
|
-
*
|
|
1523
|
-
* @param targetAgentId The agent the co-agent will voice on behalf of (drives steps 2–3).
|
|
1524
|
-
* @param explicitCoAgentId The runtime `coAgentId` mutation argument, when supplied (step 1).
|
|
1525
|
-
* @returns The resolved co-agent's id (canonical casing from the metadata cache).
|
|
1526
|
-
*/
|
|
1527
|
-
private async resolveCoAgentID(
|
|
1528
|
-
targetAgentId: string | undefined,
|
|
1529
|
-
explicitCoAgentId: string | undefined,
|
|
1530
|
-
contextUser: UserInfo,
|
|
1531
|
-
provider: IMetadataProvider,
|
|
1532
|
-
): Promise<string> {
|
|
1533
|
-
await AIEngine.Instance.Config(false, contextUser, provider);
|
|
1534
|
-
|
|
1535
|
-
// Step 1 — explicit runtime parameter: fail LOUD on any problem.
|
|
1536
|
-
if (explicitCoAgentId) {
|
|
1537
|
-
const { agent, problem } = this.findValidCoAgent(explicitCoAgentId);
|
|
1538
|
-
if (!agent) {
|
|
1539
|
-
throw new Error(`Invalid coAgentId '${explicitCoAgentId}': ${problem}`);
|
|
1540
|
-
}
|
|
1541
|
-
return agent.ID;
|
|
1542
|
-
}
|
|
1543
|
-
|
|
1544
|
-
const targetAgent = targetAgentId
|
|
1545
|
-
? (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, targetAgentId))
|
|
1546
|
-
: undefined;
|
|
1547
|
-
|
|
1548
|
-
// Step 2 — the target agent's own DefaultCoAgentID (per-agent persona): warn + fall through.
|
|
1549
|
-
const fromAgent = this.resolveMetadataDefault(
|
|
1550
|
-
targetAgent?.DefaultCoAgentID,
|
|
1551
|
-
`agent '${targetAgent?.Name}' (DefaultCoAgentID)`,
|
|
1552
|
-
);
|
|
1553
|
-
if (fromAgent) {
|
|
1554
|
-
return fromAgent;
|
|
1555
|
-
}
|
|
1556
|
-
|
|
1557
|
-
// Step 3 — the type-level AIAgentCoAgent default row (per-type default): warn + fall through.
|
|
1558
|
-
const agentType = targetAgent?.TypeID
|
|
1559
|
-
? (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, targetAgent.TypeID))
|
|
1560
|
-
: undefined;
|
|
1561
|
-
const typeDefaultCoAgentID = agentType
|
|
1562
|
-
? this.findTypeDefaultCoAgentID(agentType.ID, contextUser, provider)
|
|
1563
|
-
: undefined;
|
|
1564
|
-
const fromType = this.resolveMetadataDefault(
|
|
1565
|
-
await typeDefaultCoAgentID,
|
|
1566
|
-
`agent type '${agentType?.Name}' (AIAgentCoAgent type-default row)`,
|
|
1567
|
-
);
|
|
1568
|
-
if (fromType) {
|
|
1569
|
-
return fromType;
|
|
1570
|
-
}
|
|
1571
|
-
|
|
1572
|
-
// Step 4 — global default: the seeded Realtime Co-Agent, by name (original behavior).
|
|
1573
|
-
return this.resolveGlobalCoAgentID();
|
|
1574
|
-
}
|
|
1575
|
-
|
|
1576
|
-
/**
|
|
1577
|
-
* Chain step 3 lookup: the TYPE-LEVEL default co-agent for an agent type — the Active
|
|
1578
|
-
* `AIAgentCoAgent` row of Type `'CoAgent'` whose `TargetAgentTypeID` matches, with
|
|
1579
|
-
* `IsDefault = 1` preferred and the lowest `Sequence` breaking ties (so a deployment can
|
|
1580
|
-
* stage multiple type-level candidates deterministically). Reads {@link AIEngineBase}'s
|
|
1581
|
-
* cached rows — no RunView. Tolerant: a failed cache read logs and returns `undefined`
|
|
1582
|
-
* (the chain falls through to the global default).
|
|
1583
|
-
*/
|
|
1584
|
-
private async findTypeDefaultCoAgentID(
|
|
1585
|
-
agentTypeID: string,
|
|
1586
|
-
contextUser: UserInfo,
|
|
1587
|
-
provider: IMetadataProvider,
|
|
1588
|
-
): Promise<string | undefined> {
|
|
1589
|
-
try {
|
|
1590
|
-
const engine = await this.configuredAIEngineBase(contextUser, provider);
|
|
1591
|
-
const candidates = (engine.AgentCoAgents ?? [])
|
|
1592
|
-
.filter((r) =>
|
|
1593
|
-
r.Type === 'CoAgent' &&
|
|
1594
|
-
r.Status === 'Active' &&
|
|
1595
|
-
r.TargetAgentTypeID != null &&
|
|
1596
|
-
UUIDsEqual(r.TargetAgentTypeID, agentTypeID))
|
|
1597
|
-
.sort((a, b) =>
|
|
1598
|
-
(a.IsDefault === b.IsDefault ? 0 : a.IsDefault ? -1 : 1) ||
|
|
1599
|
-
(a.Sequence ?? 0) - (b.Sequence ?? 0));
|
|
1600
|
-
return candidates[0]?.CoAgentID;
|
|
1601
|
-
} catch (error) {
|
|
1602
|
-
LogError(
|
|
1603
|
-
`StartRealtimeClientSession: ${CO_AGENT_ENTITY} cache read failed while resolving the type-level ` +
|
|
1604
|
-
`default co-agent for agent type ${agentTypeID} (${(error as Error).message}) — falling through.`,
|
|
1605
|
-
);
|
|
1606
|
-
return undefined;
|
|
1607
|
-
}
|
|
1608
|
-
}
|
|
1609
|
-
|
|
1610
|
-
/**
|
|
1611
|
-
* Validates one metadata-level co-agent default (chain steps 2/3). Returns the resolved
|
|
1612
|
-
* co-agent id when the reference is valid; logs a warning and returns `null` (caller falls
|
|
1613
|
-
* through to the next chain step) when the reference is set but invalid — metadata drift must
|
|
1614
|
-
* degrade, not break live calls. A `null`/absent reference returns `null` silently.
|
|
1615
|
-
*/
|
|
1616
|
-
private resolveMetadataDefault(candidateId: string | null | undefined, source: string): string | null {
|
|
1617
|
-
if (!candidateId) {
|
|
1618
|
-
return null;
|
|
1619
|
-
}
|
|
1620
|
-
const { agent, problem } = this.findValidCoAgent(candidateId);
|
|
1621
|
-
if (agent) {
|
|
1622
|
-
return agent.ID;
|
|
1623
|
-
}
|
|
1624
|
-
LogError(
|
|
1625
|
-
`StartRealtimeClientSession: ignoring co-agent default '${candidateId}' from ${source} — ${problem} ` +
|
|
1626
|
-
'Falling through to the next step of the co-agent resolution chain.',
|
|
1627
|
-
);
|
|
1628
|
-
return null;
|
|
1629
|
-
}
|
|
1630
|
-
|
|
1631
|
-
/**
|
|
1632
|
-
* Validates a co-agent candidate against {@link AIEngine}'s cached metadata. A valid co-agent
|
|
1633
|
-
* must (a) exist, (b) have Status `Active`, and (c) be of the {@link REALTIME_AGENT_TYPE_NAME}
|
|
1634
|
-
* agent type. Returns the cached agent on success, or a human-readable `problem` on failure —
|
|
1635
|
-
* the CALLER decides whether that's fatal (explicit runtime param) or tolerated (metadata default).
|
|
1636
|
-
*/
|
|
1637
|
-
private findValidCoAgent(candidateId: string): { agent?: MJAIAgentEntityExtended; problem?: string } {
|
|
1638
|
-
const agent = (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, candidateId));
|
|
1639
|
-
if (!agent) {
|
|
1640
|
-
return { problem: 'no agent with that ID exists.' };
|
|
1641
|
-
}
|
|
1642
|
-
if (agent.Status !== 'Active') {
|
|
1643
|
-
return { problem: `agent '${agent.Name}' is not Active (Status: ${agent.Status}).` };
|
|
1644
|
-
}
|
|
1645
|
-
const realtimeType = (AIEngine.Instance.AgentTypes ?? []).find(
|
|
1646
|
-
t => t.Name?.trim().toLowerCase() === REALTIME_AGENT_TYPE_NAME.toLowerCase(),
|
|
1647
|
-
);
|
|
1648
|
-
if (!realtimeType) {
|
|
1649
|
-
return { problem: `the '${REALTIME_AGENT_TYPE_NAME}' agent type is not configured in this deployment.` };
|
|
1650
|
-
}
|
|
1651
|
-
if (!agent.TypeID || !UUIDsEqual(agent.TypeID, realtimeType.ID)) {
|
|
1652
|
-
return { problem: `agent '${agent.Name}' is not of the '${REALTIME_AGENT_TYPE_NAME}' agent type.` };
|
|
1653
|
-
}
|
|
1654
|
-
return { agent };
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
/**
|
|
1658
|
-
* Resolves the seeded Realtime Co-Agent's id from {@link AIEngine}'s cached agents — the GLOBAL
|
|
1659
|
-
* DEFAULT (step 4) of the co-agent resolution chain. The engine is already configured by
|
|
1660
|
-
* {@link resolveCoAgentID}. Looks up the current {@link REALTIME_CO_AGENT_NAME} first, then
|
|
1661
|
-
* falls back to the DEPRECATED {@link LEGACY_REALTIME_CO_AGENT_NAME} (pre-rename seed) with a
|
|
1662
|
-
* deprecation log so un-resynced deployments keep working. Throws a clear error when neither
|
|
1663
|
-
* name is present in metadata.
|
|
1664
|
-
*/
|
|
1665
|
-
private resolveGlobalCoAgentID(): string {
|
|
1666
|
-
const coAgent = this.findAgentByName(REALTIME_CO_AGENT_NAME);
|
|
1667
|
-
if (coAgent) {
|
|
1668
|
-
return coAgent.ID;
|
|
1669
|
-
}
|
|
1670
|
-
const legacy = this.findAgentByName(LEGACY_REALTIME_CO_AGENT_NAME);
|
|
1671
|
-
if (legacy) {
|
|
1672
|
-
LogStatus(
|
|
1673
|
-
`StartRealtimeClientSession: resolved the global co-agent via its DEPRECATED legacy name ` +
|
|
1674
|
-
`'${LEGACY_REALTIME_CO_AGENT_NAME}'. Re-sync the agent seed metadata to rename it to ` +
|
|
1675
|
-
`'${REALTIME_CO_AGENT_NAME}'.`,
|
|
1676
|
-
);
|
|
1677
|
-
return legacy.ID;
|
|
1678
|
-
}
|
|
1679
|
-
throw new Error(
|
|
1680
|
-
`The '${REALTIME_CO_AGENT_NAME}' agent is not configured; cannot start a realtime voice session.`,
|
|
1681
|
-
);
|
|
1682
|
-
}
|
|
1683
|
-
|
|
1684
|
-
/** Case/whitespace-insensitive agent lookup by Name in {@link AIEngine}'s cached agents. */
|
|
1685
|
-
private findAgentByName(name: string): MJAIAgentEntityExtended | undefined {
|
|
1686
|
-
const wanted = name.toLowerCase();
|
|
1687
|
-
return (AIEngine.Instance.Agents ?? []).find(a => a.Name?.trim().toLowerCase() === wanted);
|
|
1688
|
-
}
|
|
1689
|
-
|
|
1690
1471
|
/**
|
|
1691
1472
|
* Mints the client session config for a just-created session. On failure, closes the session so
|
|
1692
1473
|
* no half-open record leaks, then throws the service's error message.
|
|
@@ -1812,7 +1593,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1812
1593
|
const saved = await session.Save();
|
|
1813
1594
|
if (!saved) {
|
|
1814
1595
|
LogError(
|
|
1815
|
-
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed
|
|
1596
|
+
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
1816
1597
|
);
|
|
1817
1598
|
}
|
|
1818
1599
|
}
|
|
@@ -1930,7 +1711,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1930
1711
|
if (!saved) {
|
|
1931
1712
|
LogError(
|
|
1932
1713
|
`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1933
|
-
`${row
|
|
1714
|
+
`${this.describeSaveFailure(row)}`,
|
|
1934
1715
|
);
|
|
1935
1716
|
}
|
|
1936
1717
|
return saved;
|
|
@@ -2313,7 +2094,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2313
2094
|
artifact.UserID = contextUser.ID;
|
|
2314
2095
|
artifact.Visibility = 'Always';
|
|
2315
2096
|
if (!(await artifact.Save())) {
|
|
2316
|
-
const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact
|
|
2097
|
+
const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
|
|
2317
2098
|
LogError(message);
|
|
2318
2099
|
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
2319
2100
|
}
|
|
@@ -2325,7 +2106,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2325
2106
|
version.Content = contentJson;
|
|
2326
2107
|
version.UserID = contextUser.ID;
|
|
2327
2108
|
if (!(await version.Save())) {
|
|
2328
|
-
const message = `SaveSessionChannelArtifact: artifact version save failed
|
|
2109
|
+
const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
|
|
2329
2110
|
LogError(message);
|
|
2330
2111
|
return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
|
|
2331
2112
|
}
|
|
@@ -2383,13 +2164,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2383
2164
|
* queries (which scan junctions across ALL details) still surface the artifact. This is the
|
|
2384
2165
|
* least-invasive correct anchor — no fake visible message, no orphaned artifact.
|
|
2385
2166
|
*
|
|
2167
|
+
* TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
|
|
2168
|
+
* itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
|
|
2169
|
+
* (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
|
|
2170
|
+
* caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
|
|
2171
|
+
* `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
|
|
2172
|
+
* refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
|
|
2173
|
+
* issue #4791) — only the actual caller can create it.
|
|
2174
|
+
*
|
|
2386
2175
|
* Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
|
|
2387
2176
|
* because history linking did.
|
|
2388
2177
|
*/
|
|
2389
2178
|
private async linkDelegatedArtifactsToConversation(
|
|
2390
2179
|
session: MJAIAgentSessionEntity,
|
|
2391
2180
|
artifacts: DelegatedRunArtifact[] | undefined,
|
|
2392
|
-
|
|
2181
|
+
runUser: UserInfo,
|
|
2182
|
+
callerUser: UserInfo,
|
|
2393
2183
|
provider: IMetadataProvider,
|
|
2394
2184
|
): Promise<void> {
|
|
2395
2185
|
if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
|
|
@@ -2397,13 +2187,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2397
2187
|
}
|
|
2398
2188
|
try {
|
|
2399
2189
|
const detailID =
|
|
2400
|
-
(await this.findLatestSessionDetailID(session,
|
|
2401
|
-
(await this.createHiddenSessionAnchorDetail(session,
|
|
2190
|
+
(await this.findLatestSessionDetailID(session, runUser, provider)) ??
|
|
2191
|
+
(await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
|
|
2402
2192
|
if (!detailID) {
|
|
2403
2193
|
return; // anchor unavailable — logged in the helpers
|
|
2404
2194
|
}
|
|
2405
2195
|
for (const artifact of artifacts) {
|
|
2406
|
-
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID,
|
|
2196
|
+
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
|
|
2407
2197
|
}
|
|
2408
2198
|
} catch (error) {
|
|
2409
2199
|
LogError(`ExecuteRealtimeSessionTool: delegated-artifact history link failed: ${(error as Error).message}`);
|
|
@@ -2444,25 +2234,25 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2444
2234
|
*/
|
|
2445
2235
|
private async createHiddenSessionAnchorDetail(
|
|
2446
2236
|
session: MJAIAgentSessionEntity,
|
|
2447
|
-
|
|
2237
|
+
callerUser: UserInfo,
|
|
2448
2238
|
provider: IMetadataProvider,
|
|
2449
2239
|
): Promise<string | null> {
|
|
2450
|
-
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY,
|
|
2240
|
+
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
2451
2241
|
detail.NewRecord();
|
|
2452
2242
|
detail.ConversationID = session.ConversationID;
|
|
2453
2243
|
detail.Role = 'AI';
|
|
2454
2244
|
detail.HiddenToUser = true;
|
|
2455
2245
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
2456
2246
|
detail.AgentSessionID = session.ID;
|
|
2457
|
-
// Attribute
|
|
2458
|
-
//
|
|
2247
|
+
// Attribute to the SESSION owner — the caller writing this row, whose ownership
|
|
2248
|
+
// `loadOwnedActiveSession` already proved.
|
|
2459
2249
|
detail.UserID = session.UserID;
|
|
2460
2250
|
if (await detail.Save()) {
|
|
2461
2251
|
return detail.ID;
|
|
2462
2252
|
}
|
|
2463
2253
|
LogError(
|
|
2464
2254
|
`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
|
|
2465
|
-
`${detail
|
|
2255
|
+
`${this.describeSaveFailure(detail)}`,
|
|
2466
2256
|
);
|
|
2467
2257
|
return null;
|
|
2468
2258
|
}
|
|
@@ -2489,7 +2279,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2489
2279
|
if (!saved) {
|
|
2490
2280
|
LogError(
|
|
2491
2281
|
`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
|
|
2492
|
-
`${junction
|
|
2282
|
+
`${this.describeSaveFailure(junction)}`,
|
|
2493
2283
|
);
|
|
2494
2284
|
}
|
|
2495
2285
|
return saved;
|
|
@@ -2529,7 +2319,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2529
2319
|
if (!(await row.Save())) {
|
|
2530
2320
|
LogError(
|
|
2531
2321
|
`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
2532
|
-
`${row
|
|
2322
|
+
`${this.describeSaveFailure(row)}`,
|
|
2533
2323
|
);
|
|
2534
2324
|
}
|
|
2535
2325
|
} catch (error) {
|
|
@@ -2673,12 +2463,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2673
2463
|
return minutes && minutes > 0 ? minutes * 60 : undefined;
|
|
2674
2464
|
}
|
|
2675
2465
|
|
|
2676
|
-
/**
|
|
2677
|
-
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2678
|
-
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2679
|
-
*
|
|
2680
|
-
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
2681
|
-
*/
|
|
2682
2466
|
/**
|
|
2683
2467
|
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
2684
2468
|
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
@@ -2705,11 +2489,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2705
2489
|
session.RecordingMedia = 'Audio';
|
|
2706
2490
|
if (!(await session.Save())) {
|
|
2707
2491
|
LogError(
|
|
2708
|
-
`RealtimeClientSessionResolver.stampRecordingStart save failed
|
|
2492
|
+
`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
2709
2493
|
);
|
|
2710
2494
|
}
|
|
2711
2495
|
}
|
|
2712
2496
|
|
|
2497
|
+
/**
|
|
2498
|
+
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2499
|
+
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2500
|
+
*
|
|
2501
|
+
* @returns The boolean save result. On failure it logs the session, the write user and the
|
|
2502
|
+
* reason from {@link describeSaveFailure}.
|
|
2503
|
+
*/
|
|
2713
2504
|
private async persistTranscriptTurn(
|
|
2714
2505
|
session: MJAIAgentSessionEntity,
|
|
2715
2506
|
role: string,
|
|
@@ -2739,7 +2530,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2739
2530
|
const saved = await detail.Save();
|
|
2740
2531
|
if (!saved) {
|
|
2741
2532
|
LogError(
|
|
2742
|
-
`RealtimeClientSessionResolver.persistTranscriptTurn save failed
|
|
2533
|
+
`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2743
2534
|
);
|
|
2744
2535
|
}
|
|
2745
2536
|
return saved;
|
|
@@ -2780,8 +2571,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2780
2571
|
// ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
|
|
2781
2572
|
// `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
|
|
2782
2573
|
// placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
|
|
2783
|
-
// to an Owner").
|
|
2784
|
-
//
|
|
2574
|
+
// to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
|
|
2575
|
+
// full CRUD on `MJ: Conversation Details`. It is refused anyway:
|
|
2576
|
+
// `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
|
|
2577
|
+
// an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
|
|
2578
|
+
// MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
|
|
2579
|
+
// `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
|
|
2580
|
+
// denial itself still happens; that fix only made it stop being silent.
|
|
2785
2581
|
const writeUser = contextUser;
|
|
2786
2582
|
const rv = RunView.FromMetadataProvider(provider);
|
|
2787
2583
|
const result = await rv.RunView<MJConversationDetailEntity>(
|
|
@@ -2802,8 +2598,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2802
2598
|
//
|
|
2803
2599
|
// `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
|
|
2804
2600
|
// context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
|
|
2805
|
-
// principal and failed with an EMPTY message ("unknown error"), which is what
|
|
2806
|
-
//
|
|
2601
|
+
// principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
|
|
2602
|
+
// failure here looked like before the re-load. The insert path beside this one has
|
|
2807
2603
|
// always used `GetEntityObject`; this now matches it, which is also why they now succeed
|
|
2808
2604
|
// and fail for the same reasons.
|
|
2809
2605
|
const previous = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
@@ -2835,7 +2631,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2835
2631
|
`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
|
|
2836
2632
|
+ `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
|
|
2837
2633
|
+ `transcript now understates what was said: `
|
|
2838
|
-
+ `${
|
|
2634
|
+
+ `${this.describeSaveFailure(previous)}`,
|
|
2839
2635
|
);
|
|
2840
2636
|
}
|
|
2841
2637
|
return saved;
|
|
@@ -2855,6 +2651,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2855
2651
|
* Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
|
|
2856
2652
|
* Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
|
|
2857
2653
|
* on load (Option 1) without polluting user-facing speech chat bubbles.
|
|
2654
|
+
*
|
|
2655
|
+
* Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
|
|
2656
|
+
* (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
|
|
2657
|
+
* the conversation: the conversation id was client-supplied at session start. What authorizes
|
|
2658
|
+
* this write is the save itself, evaluated for the caller — the conversation owner gate in
|
|
2659
|
+
* `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
|
|
2660
|
+
* on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
|
|
2661
|
+
* `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
|
|
2662
|
+
* writing as the caller is safe even though the conversation id originated client-side.
|
|
2663
|
+
* Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
|
|
2664
|
+
* because the System user is not the conversation owner. Measured on MJ#4791 (before the
|
|
2665
|
+
* core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
|
|
2666
|
+
* `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
|
|
2667
|
+
* what silently dropped both hidden tool-execution turns of a real magic-link session; the
|
|
2668
|
+
* denial itself still happens today, only its silence was fixed separately.
|
|
2669
|
+
* `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
|
|
2670
|
+
* runs, the relayed dispatch itself) — never this one.
|
|
2858
2671
|
*/
|
|
2859
2672
|
private async persistDirectActionTurn(
|
|
2860
2673
|
session: MJAIAgentSessionEntity,
|
|
@@ -2870,10 +2683,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2870
2683
|
return false;
|
|
2871
2684
|
}
|
|
2872
2685
|
try {
|
|
2873
|
-
const writeUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
2874
2686
|
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
2875
2687
|
CONVERSATION_DETAIL_ENTITY,
|
|
2876
|
-
|
|
2688
|
+
contextUser,
|
|
2877
2689
|
);
|
|
2878
2690
|
detail.NewRecord();
|
|
2879
2691
|
detail.ConversationID = session.ConversationID;
|
|
@@ -2915,13 +2727,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2915
2727
|
const saved = await detail.Save();
|
|
2916
2728
|
if (!saved) {
|
|
2917
2729
|
LogError(
|
|
2918
|
-
`RealtimeClientSessionResolver.persistDirectActionTurn save failed
|
|
2730
|
+
`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2919
2731
|
);
|
|
2920
2732
|
}
|
|
2921
2733
|
return saved;
|
|
2922
2734
|
} catch (error) {
|
|
2923
2735
|
const message = error instanceof Error ? error.message : String(error);
|
|
2924
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
|
|
2736
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
|
|
2925
2737
|
return false;
|
|
2926
2738
|
}
|
|
2927
2739
|
}
|