@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
|
@@ -41,7 +41,7 @@ import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjun
|
|
|
41
41
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
42
42
|
import { AIEngine } from '@memberjunction/aiengine';
|
|
43
43
|
import { AIAgentPermissionHelper, AIEngineBase } from '@memberjunction/ai-engine-base';
|
|
44
|
-
import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
|
|
44
|
+
import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, FindIgnoredRealtimeConfigKeys, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, ResolveRealtimeCoAgentID, FilterAllowedAgentsByCanRun, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
|
|
45
45
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
46
46
|
import { GetReadWriteProvider } from '../util.js';
|
|
47
47
|
import { SessionManager } from '../agentSessions/index.js';
|
|
@@ -52,29 +52,6 @@ import { ResolveWidgetGuestRunContext, ResolveScopedAnonymousRunUser } from '../
|
|
|
52
52
|
* noise is dropped so the model only narrates meaningful work.
|
|
53
53
|
*/
|
|
54
54
|
const SIGNIFICANT_PROGRESS_STEPS = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
55
|
-
/**
|
|
56
|
-
* The seeded name of the internal orchestration agent that fronts a target agent in realtime
|
|
57
|
-
* sessions (voice + interactive channels). This is the GLOBAL DEFAULT co-agent — the final step of
|
|
58
|
-
* the co-agent resolution chain (see {@link RealtimeClientSessionResolver.resolveCoAgentID}).
|
|
59
|
-
* Deployments can override it per agent (`AIAgent.DefaultCoAgentID`), per agent type (an
|
|
60
|
-
* `AIAgentCoAgent` row with `TargetAgentTypeID` + `IsDefault`), or per call (the `coAgentId`
|
|
61
|
-
* mutation argument) without
|
|
62
|
-
* touching this seed.
|
|
63
|
-
*/
|
|
64
|
-
const REALTIME_CO_AGENT_NAME = 'Realtime Co-Agent';
|
|
65
|
-
/**
|
|
66
|
-
* DEPRECATED legacy seed name of {@link REALTIME_CO_AGENT_NAME}, from before the agent's rename
|
|
67
|
-
* from "Voice Co-Agent" to "Realtime Co-Agent". Deployments that have not re-synced the agent seed
|
|
68
|
-
* still carry this name, so {@link RealtimeClientSessionResolver.resolveGlobalCoAgentID} falls
|
|
69
|
-
* back to it (with a deprecation log) when no agent named {@link REALTIME_CO_AGENT_NAME} exists.
|
|
70
|
-
*/
|
|
71
|
-
const LEGACY_REALTIME_CO_AGENT_NAME = 'Voice Co-Agent';
|
|
72
|
-
/**
|
|
73
|
-
* The seeded name of the Realtime agent TYPE. Every co-agent candidate (explicit or metadata
|
|
74
|
-
* default) must be an Active agent of this type — a co-agent drives a streaming full-duplex
|
|
75
|
-
* realtime session, which only `RealtimeAgentType`-driven agents support.
|
|
76
|
-
*/
|
|
77
|
-
const REALTIME_AGENT_TYPE_NAME = 'Realtime';
|
|
78
55
|
/** Entity name — centralised so the `MJ:`-prefix convention is applied in exactly one place. */
|
|
79
56
|
const SESSION_ENTITY = 'MJ: AI Agent Sessions';
|
|
80
57
|
const CO_AGENT_ENTITY = 'MJ: AI Agent Co Agents';
|
|
@@ -270,6 +247,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
270
247
|
this.clientSessionService = new RealtimeClientSessionService();
|
|
271
248
|
this.sessionManager = new SessionManager(this.clientSessionService);
|
|
272
249
|
}
|
|
250
|
+
/**
|
|
251
|
+
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
252
|
+
* `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
|
|
253
|
+
* runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
|
|
254
|
+
* When a subclass refused the write without recording anything, say so explicitly instead of
|
|
255
|
+
* "unknown error", which is what hid MJ#4791.
|
|
256
|
+
*
|
|
257
|
+
* Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
|
|
258
|
+
* registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
|
|
259
|
+
* refusal registered nothing — so its text must never be reported as this failure's reason.
|
|
260
|
+
*/
|
|
261
|
+
describeSaveFailure(entity) {
|
|
262
|
+
const latest = entity.LatestResult;
|
|
263
|
+
if (!latest) {
|
|
264
|
+
return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
|
|
265
|
+
'— the write was refused before reaching the provider';
|
|
266
|
+
}
|
|
267
|
+
if (latest.Success) {
|
|
268
|
+
return 'no failure detail recorded (latest result entry is a prior success; ' +
|
|
269
|
+
`ResultHistory length ${entity.ResultHistory.length})`;
|
|
270
|
+
}
|
|
271
|
+
const detail = latest.CompleteMessage?.trim();
|
|
272
|
+
return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
|
|
273
|
+
}
|
|
273
274
|
/**
|
|
274
275
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
275
276
|
*
|
|
@@ -278,7 +279,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
278
279
|
* session is created.
|
|
279
280
|
* 2. Resolve the co-agent id via the metadata-driven resolution chain (runtime `coAgentId` →
|
|
280
281
|
* agent's `DefaultCoAgentID` → type-level `AIAgentCoAgent` default row → global Realtime Co-Agent) —
|
|
281
|
-
* see {@link
|
|
282
|
+
* see {@link ResolveRealtimeCoAgentID} for the full contract.
|
|
282
283
|
* 3. Create the durable `AIAgentSession` (run by the co-agent), storing `targetAgentID` in its
|
|
283
284
|
* config server-side — this is the authoritative target for all later relays.
|
|
284
285
|
* 4. Mint the {@link import('@memberjunction/ai').ClientRealtimeSessionConfig} via the service.
|
|
@@ -311,7 +312,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
311
312
|
*/
|
|
312
313
|
async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson, recordingStartedAt, recordingConsent, mediaCollectionId, applicationId, appContextJson) {
|
|
313
314
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
314
|
-
const coAgentID = await
|
|
315
|
+
const coAgentID = await ResolveRealtimeCoAgentID(targetAgentId, coAgentId, contextUser, provider);
|
|
315
316
|
// PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
|
|
316
317
|
// (with the IsDefault row standing in when no runtime target was supplied); zero rows =
|
|
317
318
|
// universal, today's behavior untouched. Resolves the AUTHORITATIVE target id.
|
|
@@ -398,7 +399,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
398
399
|
TargetAgentID: config.targetAgentID,
|
|
399
400
|
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
400
401
|
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
401
|
-
AllowedAgents: await
|
|
402
|
+
AllowedAgents: await FilterAllowedAgentsByCanRun(config.allowedAgents, contextUser),
|
|
402
403
|
DirectActions: config.directActions,
|
|
403
404
|
// Attribution follows the VISITOR even when `runUser` is elevated: the delegated run
|
|
404
405
|
// row and its context-memory scope must stay the person's, not the system user's.
|
|
@@ -421,7 +422,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
421
422
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
422
423
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
423
424
|
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
424
|
-
|
|
425
|
+
// The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
|
|
426
|
+
// — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
|
|
427
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
|
|
425
428
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
426
429
|
return ResultJson;
|
|
427
430
|
}
|
|
@@ -472,7 +475,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
472
475
|
};
|
|
473
476
|
session.Config_ = JSON.stringify(next);
|
|
474
477
|
if (!(await session.Save())) {
|
|
475
|
-
LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed
|
|
478
|
+
LogError(`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
476
479
|
}
|
|
477
480
|
}
|
|
478
481
|
/**
|
|
@@ -906,33 +909,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
906
909
|
throw new Error(`Not authorized: you are not permitted to run the target agent ${targetAgentId}`);
|
|
907
910
|
}
|
|
908
911
|
}
|
|
909
|
-
/**
|
|
910
|
-
* Narrows a session's colleague union (`allowedAgents`) to the agents the CALLER may run.
|
|
911
|
-
*
|
|
912
|
-
* {@link assertCanRunTarget} gates the LEAD target at session start, but the union it travels
|
|
913
|
-
* with was never gated at all — a model-named colleague resolves straight to a delegated run.
|
|
914
|
-
* That was survivable while the run carried the caller's own identity, because base-agent
|
|
915
|
-
* re-checks `CanRun` against `contextUser`. Once the run user is elevated for a scoped anonymous
|
|
916
|
-
* caller (issue #3371) that check sees the SYSTEM user, so this is the only remaining place the
|
|
917
|
-
* caller's own authority is applied to a colleague. It therefore runs for EVERY caller, elevated
|
|
918
|
-
* or not — the authorization identity must never depend on the elevation decision.
|
|
919
|
-
*
|
|
920
|
-
* `HasPermission` reads AIEngineBase's in-memory caches (no DB round trip) and already fails
|
|
921
|
-
* closed on error, so an unresolvable agent drops OUT of the union rather than becoming runnable.
|
|
922
|
-
* A filtered-out colleague is not an error: the delegation layer reports it as "not available in
|
|
923
|
-
* this session" and lists what remains, which is the same answer the model gets for a typo.
|
|
924
|
-
*
|
|
925
|
-
* @param allowedAgents The session's persisted colleague union (absent/empty ⇒ single-target).
|
|
926
|
-
* @param contextUser The ORIGINAL caller — never the elevated run user.
|
|
927
|
-
* @returns The subset the caller may run, preserving order.
|
|
928
|
-
*/
|
|
929
|
-
async filterAllowedAgentsByCanRun(allowedAgents, contextUser) {
|
|
930
|
-
if (!allowedAgents || allowedAgents.length === 0) {
|
|
931
|
-
return allowedAgents;
|
|
932
|
-
}
|
|
933
|
-
const verdicts = await Promise.all(allowedAgents.map((a) => AIAgentPermissionHelper.HasPermission(a.agentId, contextUser, 'run')));
|
|
934
|
-
return allowedAgents.filter((_, i) => verdicts[i]);
|
|
935
|
-
}
|
|
936
912
|
/**
|
|
937
913
|
* Resolves the AUTHORITATIVE target agent id under the co-agent's PAIRING CONSTRAINTS
|
|
938
914
|
* (`MJ: AI Agent Co Agents`, ordered by `Sequence`):
|
|
@@ -1136,158 +1112,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1136
1112
|
return false;
|
|
1137
1113
|
}
|
|
1138
1114
|
}
|
|
1139
|
-
/**
|
|
1140
|
-
* Resolves the co-agent (the Realtime-type agent that voices the target agent) for a new
|
|
1141
|
-
* client-direct session via the metadata-driven **CO-AGENT RESOLUTION CHAIN** — first match
|
|
1142
|
-
* wins, evaluated in precedence order:
|
|
1143
|
-
*
|
|
1144
|
-
* 1. **Runtime parameter** — the mutation's explicit `coAgentId`. A per-call override; an
|
|
1145
|
-
* invalid candidate (unknown, not Active, or not of the Realtime agent type) **throws**
|
|
1146
|
-
* (fail loud — the caller asked for something specific, mirroring `preferredModelId`).
|
|
1147
|
-
* 2. **Per-agent persona** — the target agent's `AIAgent.DefaultCoAgentID`. An invalid
|
|
1148
|
-
* reference logs a warning and **falls through** to the next step (stale metadata should
|
|
1149
|
-
* degrade gracefully, never break calls).
|
|
1150
|
-
* 3. **Per-type default** — an Active `AIAgentCoAgent` row of Type `'CoAgent'` whose
|
|
1151
|
-
* `TargetAgentTypeID` is the target agent's type and `IsDefault = 1` (lowest `Sequence`
|
|
1152
|
-
* wins a tie). Same tolerant warn-and-fall-through semantics as step 2.
|
|
1153
|
-
* 4. **Global default** — the seeded {@link REALTIME_CO_AGENT_NAME} agent, looked up by name
|
|
1154
|
-
* (with a deprecated fallback to {@link LEGACY_REALTIME_CO_AGENT_NAME}). Throws when absent
|
|
1155
|
-
* entirely (the realtime feature is unconfigured in this deployment).
|
|
1156
|
-
*
|
|
1157
|
-
* Every candidate from steps 1–3 is validated by {@link findValidCoAgent}: it must exist in
|
|
1158
|
-
* {@link AIEngine}'s cached agents, have Status `Active`, and be of the
|
|
1159
|
-
* {@link REALTIME_AGENT_TYPE_NAME} agent type. Step 4 keeps its original name-lookup contract.
|
|
1160
|
-
*
|
|
1161
|
-
* @param targetAgentId The agent the co-agent will voice on behalf of (drives steps 2–3).
|
|
1162
|
-
* @param explicitCoAgentId The runtime `coAgentId` mutation argument, when supplied (step 1).
|
|
1163
|
-
* @returns The resolved co-agent's id (canonical casing from the metadata cache).
|
|
1164
|
-
*/
|
|
1165
|
-
async resolveCoAgentID(targetAgentId, explicitCoAgentId, contextUser, provider) {
|
|
1166
|
-
await AIEngine.Instance.Config(false, contextUser, provider);
|
|
1167
|
-
// Step 1 — explicit runtime parameter: fail LOUD on any problem.
|
|
1168
|
-
if (explicitCoAgentId) {
|
|
1169
|
-
const { agent, problem } = this.findValidCoAgent(explicitCoAgentId);
|
|
1170
|
-
if (!agent) {
|
|
1171
|
-
throw new Error(`Invalid coAgentId '${explicitCoAgentId}': ${problem}`);
|
|
1172
|
-
}
|
|
1173
|
-
return agent.ID;
|
|
1174
|
-
}
|
|
1175
|
-
const targetAgent = targetAgentId
|
|
1176
|
-
? (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, targetAgentId))
|
|
1177
|
-
: undefined;
|
|
1178
|
-
// Step 2 — the target agent's own DefaultCoAgentID (per-agent persona): warn + fall through.
|
|
1179
|
-
const fromAgent = this.resolveMetadataDefault(targetAgent?.DefaultCoAgentID, `agent '${targetAgent?.Name}' (DefaultCoAgentID)`);
|
|
1180
|
-
if (fromAgent) {
|
|
1181
|
-
return fromAgent;
|
|
1182
|
-
}
|
|
1183
|
-
// Step 3 — the type-level AIAgentCoAgent default row (per-type default): warn + fall through.
|
|
1184
|
-
const agentType = targetAgent?.TypeID
|
|
1185
|
-
? (AIEngine.Instance.AgentTypes ?? []).find(t => UUIDsEqual(t.ID, targetAgent.TypeID))
|
|
1186
|
-
: undefined;
|
|
1187
|
-
const typeDefaultCoAgentID = agentType
|
|
1188
|
-
? this.findTypeDefaultCoAgentID(agentType.ID, contextUser, provider)
|
|
1189
|
-
: undefined;
|
|
1190
|
-
const fromType = this.resolveMetadataDefault(await typeDefaultCoAgentID, `agent type '${agentType?.Name}' (AIAgentCoAgent type-default row)`);
|
|
1191
|
-
if (fromType) {
|
|
1192
|
-
return fromType;
|
|
1193
|
-
}
|
|
1194
|
-
// Step 4 — global default: the seeded Realtime Co-Agent, by name (original behavior).
|
|
1195
|
-
return this.resolveGlobalCoAgentID();
|
|
1196
|
-
}
|
|
1197
|
-
/**
|
|
1198
|
-
* Chain step 3 lookup: the TYPE-LEVEL default co-agent for an agent type — the Active
|
|
1199
|
-
* `AIAgentCoAgent` row of Type `'CoAgent'` whose `TargetAgentTypeID` matches, with
|
|
1200
|
-
* `IsDefault = 1` preferred and the lowest `Sequence` breaking ties (so a deployment can
|
|
1201
|
-
* stage multiple type-level candidates deterministically). Reads {@link AIEngineBase}'s
|
|
1202
|
-
* cached rows — no RunView. Tolerant: a failed cache read logs and returns `undefined`
|
|
1203
|
-
* (the chain falls through to the global default).
|
|
1204
|
-
*/
|
|
1205
|
-
async findTypeDefaultCoAgentID(agentTypeID, contextUser, provider) {
|
|
1206
|
-
try {
|
|
1207
|
-
const engine = await this.configuredAIEngineBase(contextUser, provider);
|
|
1208
|
-
const candidates = (engine.AgentCoAgents ?? [])
|
|
1209
|
-
.filter((r) => r.Type === 'CoAgent' &&
|
|
1210
|
-
r.Status === 'Active' &&
|
|
1211
|
-
r.TargetAgentTypeID != null &&
|
|
1212
|
-
UUIDsEqual(r.TargetAgentTypeID, agentTypeID))
|
|
1213
|
-
.sort((a, b) => (a.IsDefault === b.IsDefault ? 0 : a.IsDefault ? -1 : 1) ||
|
|
1214
|
-
(a.Sequence ?? 0) - (b.Sequence ?? 0));
|
|
1215
|
-
return candidates[0]?.CoAgentID;
|
|
1216
|
-
}
|
|
1217
|
-
catch (error) {
|
|
1218
|
-
LogError(`StartRealtimeClientSession: ${CO_AGENT_ENTITY} cache read failed while resolving the type-level ` +
|
|
1219
|
-
`default co-agent for agent type ${agentTypeID} (${error.message}) — falling through.`);
|
|
1220
|
-
return undefined;
|
|
1221
|
-
}
|
|
1222
|
-
}
|
|
1223
|
-
/**
|
|
1224
|
-
* Validates one metadata-level co-agent default (chain steps 2/3). Returns the resolved
|
|
1225
|
-
* co-agent id when the reference is valid; logs a warning and returns `null` (caller falls
|
|
1226
|
-
* through to the next chain step) when the reference is set but invalid — metadata drift must
|
|
1227
|
-
* degrade, not break live calls. A `null`/absent reference returns `null` silently.
|
|
1228
|
-
*/
|
|
1229
|
-
resolveMetadataDefault(candidateId, source) {
|
|
1230
|
-
if (!candidateId) {
|
|
1231
|
-
return null;
|
|
1232
|
-
}
|
|
1233
|
-
const { agent, problem } = this.findValidCoAgent(candidateId);
|
|
1234
|
-
if (agent) {
|
|
1235
|
-
return agent.ID;
|
|
1236
|
-
}
|
|
1237
|
-
LogError(`StartRealtimeClientSession: ignoring co-agent default '${candidateId}' from ${source} — ${problem} ` +
|
|
1238
|
-
'Falling through to the next step of the co-agent resolution chain.');
|
|
1239
|
-
return null;
|
|
1240
|
-
}
|
|
1241
|
-
/**
|
|
1242
|
-
* Validates a co-agent candidate against {@link AIEngine}'s cached metadata. A valid co-agent
|
|
1243
|
-
* must (a) exist, (b) have Status `Active`, and (c) be of the {@link REALTIME_AGENT_TYPE_NAME}
|
|
1244
|
-
* agent type. Returns the cached agent on success, or a human-readable `problem` on failure —
|
|
1245
|
-
* the CALLER decides whether that's fatal (explicit runtime param) or tolerated (metadata default).
|
|
1246
|
-
*/
|
|
1247
|
-
findValidCoAgent(candidateId) {
|
|
1248
|
-
const agent = (AIEngine.Instance.Agents ?? []).find(a => UUIDsEqual(a.ID, candidateId));
|
|
1249
|
-
if (!agent) {
|
|
1250
|
-
return { problem: 'no agent with that ID exists.' };
|
|
1251
|
-
}
|
|
1252
|
-
if (agent.Status !== 'Active') {
|
|
1253
|
-
return { problem: `agent '${agent.Name}' is not Active (Status: ${agent.Status}).` };
|
|
1254
|
-
}
|
|
1255
|
-
const realtimeType = (AIEngine.Instance.AgentTypes ?? []).find(t => t.Name?.trim().toLowerCase() === REALTIME_AGENT_TYPE_NAME.toLowerCase());
|
|
1256
|
-
if (!realtimeType) {
|
|
1257
|
-
return { problem: `the '${REALTIME_AGENT_TYPE_NAME}' agent type is not configured in this deployment.` };
|
|
1258
|
-
}
|
|
1259
|
-
if (!agent.TypeID || !UUIDsEqual(agent.TypeID, realtimeType.ID)) {
|
|
1260
|
-
return { problem: `agent '${agent.Name}' is not of the '${REALTIME_AGENT_TYPE_NAME}' agent type.` };
|
|
1261
|
-
}
|
|
1262
|
-
return { agent };
|
|
1263
|
-
}
|
|
1264
|
-
/**
|
|
1265
|
-
* Resolves the seeded Realtime Co-Agent's id from {@link AIEngine}'s cached agents — the GLOBAL
|
|
1266
|
-
* DEFAULT (step 4) of the co-agent resolution chain. The engine is already configured by
|
|
1267
|
-
* {@link resolveCoAgentID}. Looks up the current {@link REALTIME_CO_AGENT_NAME} first, then
|
|
1268
|
-
* falls back to the DEPRECATED {@link LEGACY_REALTIME_CO_AGENT_NAME} (pre-rename seed) with a
|
|
1269
|
-
* deprecation log so un-resynced deployments keep working. Throws a clear error when neither
|
|
1270
|
-
* name is present in metadata.
|
|
1271
|
-
*/
|
|
1272
|
-
resolveGlobalCoAgentID() {
|
|
1273
|
-
const coAgent = this.findAgentByName(REALTIME_CO_AGENT_NAME);
|
|
1274
|
-
if (coAgent) {
|
|
1275
|
-
return coAgent.ID;
|
|
1276
|
-
}
|
|
1277
|
-
const legacy = this.findAgentByName(LEGACY_REALTIME_CO_AGENT_NAME);
|
|
1278
|
-
if (legacy) {
|
|
1279
|
-
LogStatus(`StartRealtimeClientSession: resolved the global co-agent via its DEPRECATED legacy name ` +
|
|
1280
|
-
`'${LEGACY_REALTIME_CO_AGENT_NAME}'. Re-sync the agent seed metadata to rename it to ` +
|
|
1281
|
-
`'${REALTIME_CO_AGENT_NAME}'.`);
|
|
1282
|
-
return legacy.ID;
|
|
1283
|
-
}
|
|
1284
|
-
throw new Error(`The '${REALTIME_CO_AGENT_NAME}' agent is not configured; cannot start a realtime voice session.`);
|
|
1285
|
-
}
|
|
1286
|
-
/** Case/whitespace-insensitive agent lookup by Name in {@link AIEngine}'s cached agents. */
|
|
1287
|
-
findAgentByName(name) {
|
|
1288
|
-
const wanted = name.toLowerCase();
|
|
1289
|
-
return (AIEngine.Instance.Agents ?? []).find(a => a.Name?.trim().toLowerCase() === wanted);
|
|
1290
|
-
}
|
|
1291
1115
|
/**
|
|
1292
1116
|
* Mints the client session config for a just-created session. On failure, closes the session so
|
|
1293
1117
|
* no half-open record leaks, then throws the service's error message.
|
|
@@ -1378,7 +1202,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1378
1202
|
session.Config_ = JSON.stringify(config);
|
|
1379
1203
|
const saved = await session.Save();
|
|
1380
1204
|
if (!saved) {
|
|
1381
|
-
LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed
|
|
1205
|
+
LogError(`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
1382
1206
|
}
|
|
1383
1207
|
}
|
|
1384
1208
|
/**
|
|
@@ -1462,7 +1286,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1462
1286
|
const saved = await row.Save();
|
|
1463
1287
|
if (!saved) {
|
|
1464
1288
|
LogError(`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1465
|
-
`${row
|
|
1289
|
+
`${this.describeSaveFailure(row)}`);
|
|
1466
1290
|
}
|
|
1467
1291
|
return saved;
|
|
1468
1292
|
}
|
|
@@ -1765,7 +1589,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1765
1589
|
artifact.UserID = contextUser.ID;
|
|
1766
1590
|
artifact.Visibility = 'Always';
|
|
1767
1591
|
if (!(await artifact.Save())) {
|
|
1768
|
-
const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact
|
|
1592
|
+
const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
|
|
1769
1593
|
LogError(message);
|
|
1770
1594
|
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
1771
1595
|
}
|
|
@@ -1776,7 +1600,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1776
1600
|
version.Content = contentJson;
|
|
1777
1601
|
version.UserID = contextUser.ID;
|
|
1778
1602
|
if (!(await version.Save())) {
|
|
1779
|
-
const message = `SaveSessionChannelArtifact: artifact version save failed
|
|
1603
|
+
const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
|
|
1780
1604
|
LogError(message);
|
|
1781
1605
|
return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
|
|
1782
1606
|
}
|
|
@@ -1826,21 +1650,29 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1826
1650
|
* queries (which scan junctions across ALL details) still surface the artifact. This is the
|
|
1827
1651
|
* least-invasive correct anchor — no fake visible message, no orphaned artifact.
|
|
1828
1652
|
*
|
|
1653
|
+
* TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
|
|
1654
|
+
* itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
|
|
1655
|
+
* (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
|
|
1656
|
+
* caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
|
|
1657
|
+
* `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
|
|
1658
|
+
* refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
|
|
1659
|
+
* issue #4791) — only the actual caller can create it.
|
|
1660
|
+
*
|
|
1829
1661
|
* Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
|
|
1830
1662
|
* because history linking did.
|
|
1831
1663
|
*/
|
|
1832
|
-
async linkDelegatedArtifactsToConversation(session, artifacts,
|
|
1664
|
+
async linkDelegatedArtifactsToConversation(session, artifacts, runUser, callerUser, provider) {
|
|
1833
1665
|
if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
|
|
1834
1666
|
return;
|
|
1835
1667
|
}
|
|
1836
1668
|
try {
|
|
1837
|
-
const detailID = (await this.findLatestSessionDetailID(session,
|
|
1838
|
-
(await this.createHiddenSessionAnchorDetail(session,
|
|
1669
|
+
const detailID = (await this.findLatestSessionDetailID(session, runUser, provider)) ??
|
|
1670
|
+
(await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
|
|
1839
1671
|
if (!detailID) {
|
|
1840
1672
|
return; // anchor unavailable — logged in the helpers
|
|
1841
1673
|
}
|
|
1842
1674
|
for (const artifact of artifacts) {
|
|
1843
|
-
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID,
|
|
1675
|
+
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
|
|
1844
1676
|
}
|
|
1845
1677
|
}
|
|
1846
1678
|
catch (error) {
|
|
@@ -1871,22 +1703,22 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1871
1703
|
* {@link linkDelegatedArtifactsToConversation}). Returns the new detail's id, or `null`
|
|
1872
1704
|
* (logged) when the save fails.
|
|
1873
1705
|
*/
|
|
1874
|
-
async createHiddenSessionAnchorDetail(session,
|
|
1875
|
-
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY,
|
|
1706
|
+
async createHiddenSessionAnchorDetail(session, callerUser, provider) {
|
|
1707
|
+
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
1876
1708
|
detail.NewRecord();
|
|
1877
1709
|
detail.ConversationID = session.ConversationID;
|
|
1878
1710
|
detail.Role = 'AI';
|
|
1879
1711
|
detail.HiddenToUser = true;
|
|
1880
1712
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
1881
1713
|
detail.AgentSessionID = session.ID;
|
|
1882
|
-
// Attribute
|
|
1883
|
-
//
|
|
1714
|
+
// Attribute to the SESSION owner — the caller writing this row, whose ownership
|
|
1715
|
+
// `loadOwnedActiveSession` already proved.
|
|
1884
1716
|
detail.UserID = session.UserID;
|
|
1885
1717
|
if (await detail.Save()) {
|
|
1886
1718
|
return detail.ID;
|
|
1887
1719
|
}
|
|
1888
1720
|
LogError(`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
|
|
1889
|
-
`${detail
|
|
1721
|
+
`${this.describeSaveFailure(detail)}`);
|
|
1890
1722
|
return null;
|
|
1891
1723
|
}
|
|
1892
1724
|
/**
|
|
@@ -1902,7 +1734,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1902
1734
|
const saved = await junction.Save();
|
|
1903
1735
|
if (!saved) {
|
|
1904
1736
|
LogError(`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
|
|
1905
|
-
`${junction
|
|
1737
|
+
`${this.describeSaveFailure(junction)}`);
|
|
1906
1738
|
}
|
|
1907
1739
|
return saved;
|
|
1908
1740
|
}
|
|
@@ -1931,7 +1763,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1931
1763
|
row.LastActiveAt = new Date();
|
|
1932
1764
|
if (!(await row.Save())) {
|
|
1933
1765
|
LogError(`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1934
|
-
`${row
|
|
1766
|
+
`${this.describeSaveFailure(row)}`);
|
|
1935
1767
|
}
|
|
1936
1768
|
}
|
|
1937
1769
|
catch (error) {
|
|
@@ -2064,12 +1896,6 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2064
1896
|
const minutes = elevation?.Widget.VoiceMaxSessionMinutes;
|
|
2065
1897
|
return minutes && minutes > 0 ? minutes * 60 : undefined;
|
|
2066
1898
|
}
|
|
2067
|
-
/**
|
|
2068
|
-
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2069
|
-
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2070
|
-
*
|
|
2071
|
-
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
2072
|
-
*/
|
|
2073
1899
|
/**
|
|
2074
1900
|
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
2075
1901
|
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
@@ -2091,9 +1917,16 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2091
1917
|
session.RecordingStartedAt = startedAt;
|
|
2092
1918
|
session.RecordingMedia = 'Audio';
|
|
2093
1919
|
if (!(await session.Save())) {
|
|
2094
|
-
LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed
|
|
1920
|
+
LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`);
|
|
2095
1921
|
}
|
|
2096
1922
|
}
|
|
1923
|
+
/**
|
|
1924
|
+
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
1925
|
+
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
1926
|
+
*
|
|
1927
|
+
* @returns The boolean save result. On failure it logs the session, the write user and the
|
|
1928
|
+
* reason from {@link describeSaveFailure}.
|
|
1929
|
+
*/
|
|
2097
1930
|
async persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
|
|
2098
1931
|
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
|
|
2099
1932
|
detail.NewRecord();
|
|
@@ -2110,7 +1943,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2110
1943
|
}
|
|
2111
1944
|
const saved = await detail.Save();
|
|
2112
1945
|
if (!saved) {
|
|
2113
|
-
LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed
|
|
1946
|
+
LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
|
|
2114
1947
|
}
|
|
2115
1948
|
return saved;
|
|
2116
1949
|
}
|
|
@@ -2141,8 +1974,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2141
1974
|
// ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
|
|
2142
1975
|
// `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
|
|
2143
1976
|
// placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
|
|
2144
|
-
// to an Owner").
|
|
2145
|
-
//
|
|
1977
|
+
// to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
|
|
1978
|
+
// full CRUD on `MJ: Conversation Details`. It is refused anyway:
|
|
1979
|
+
// `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
|
|
1980
|
+
// an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
|
|
1981
|
+
// MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
|
|
1982
|
+
// `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
|
|
1983
|
+
// denial itself still happens; that fix only made it stop being silent.
|
|
2146
1984
|
const writeUser = contextUser;
|
|
2147
1985
|
const rv = RunView.FromMetadataProvider(provider);
|
|
2148
1986
|
const result = await rv.RunView({
|
|
@@ -2160,8 +1998,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2160
1998
|
//
|
|
2161
1999
|
// `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
|
|
2162
2000
|
// context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
|
|
2163
|
-
// principal and failed with an EMPTY message ("unknown error"), which is what
|
|
2164
|
-
//
|
|
2001
|
+
// principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
|
|
2002
|
+
// failure here looked like before the re-load. The insert path beside this one has
|
|
2165
2003
|
// always used `GetEntityObject`; this now matches it, which is also why they now succeed
|
|
2166
2004
|
// and fail for the same reasons.
|
|
2167
2005
|
const previous = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
|
|
@@ -2187,7 +2025,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2187
2025
|
LogError(`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
|
|
2188
2026
|
+ `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
|
|
2189
2027
|
+ `transcript now understates what was said: `
|
|
2190
|
-
+ `${
|
|
2028
|
+
+ `${this.describeSaveFailure(previous)}`);
|
|
2191
2029
|
}
|
|
2192
2030
|
return saved;
|
|
2193
2031
|
}
|
|
@@ -2204,14 +2042,30 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2204
2042
|
* Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
|
|
2205
2043
|
* Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
|
|
2206
2044
|
* on load (Option 1) without polluting user-facing speech chat bubbles.
|
|
2045
|
+
*
|
|
2046
|
+
* Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
|
|
2047
|
+
* (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
|
|
2048
|
+
* the conversation: the conversation id was client-supplied at session start. What authorizes
|
|
2049
|
+
* this write is the save itself, evaluated for the caller — the conversation owner gate in
|
|
2050
|
+
* `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
|
|
2051
|
+
* on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
|
|
2052
|
+
* `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
|
|
2053
|
+
* writing as the caller is safe even though the conversation id originated client-side.
|
|
2054
|
+
* Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
|
|
2055
|
+
* because the System user is not the conversation owner. Measured on MJ#4791 (before the
|
|
2056
|
+
* core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
|
|
2057
|
+
* `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
|
|
2058
|
+
* what silently dropped both hidden tool-execution turns of a real magic-link session; the
|
|
2059
|
+
* denial itself still happens today, only its silence was fixed separately.
|
|
2060
|
+
* `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
|
|
2061
|
+
* runs, the relayed dispatch itself) — never this one.
|
|
2207
2062
|
*/
|
|
2208
2063
|
async persistDirectActionTurn(session, callId, toolName, argsJson, resultJson, durationMs, contextUser, provider) {
|
|
2209
2064
|
if (!session.ConversationID) {
|
|
2210
2065
|
return false;
|
|
2211
2066
|
}
|
|
2212
2067
|
try {
|
|
2213
|
-
const
|
|
2214
|
-
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, writeUser);
|
|
2068
|
+
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
|
|
2215
2069
|
detail.NewRecord();
|
|
2216
2070
|
detail.ConversationID = session.ConversationID;
|
|
2217
2071
|
detail.Role = 'AI';
|
|
@@ -2249,13 +2103,13 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
2249
2103
|
detail.Message = JSON.stringify(factPayload);
|
|
2250
2104
|
const saved = await detail.Save();
|
|
2251
2105
|
if (!saved) {
|
|
2252
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed
|
|
2106
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`);
|
|
2253
2107
|
}
|
|
2254
2108
|
return saved;
|
|
2255
2109
|
}
|
|
2256
2110
|
catch (error) {
|
|
2257
2111
|
const message = error instanceof Error ? error.message : String(error);
|
|
2258
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
|
|
2112
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
|
|
2259
2113
|
return false;
|
|
2260
2114
|
}
|
|
2261
2115
|
}
|