@memberjunction/server 5.42.0 → 5.44.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +5 -1
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +15 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +53 -11
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +3 -1
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/generated/generated.d.ts +1180 -51
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +35564 -29262
- package/dist/generated/generated.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14 -1
- package/dist/index.js.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts +20 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.d.ts.map +1 -1
- package/dist/resolvers/ExecuteRemoteOperationResolver.js +61 -5
- package/dist/resolvers/ExecuteRemoteOperationResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +74 -0
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +211 -2
- package/dist/resolvers/FileResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +98 -29
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +6 -4
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +29 -14
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +17 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +78 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +81 -2
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +293 -17
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +17 -2
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +12 -3
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +37 -15
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts +8 -0
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +8 -0
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/meetingRecordingRegistration.d.ts +124 -0
- package/dist/resolvers/meetingRecordingRegistration.d.ts.map +1 -0
- package/dist/resolvers/meetingRecordingRegistration.js +311 -0
- package/dist/resolvers/meetingRecordingRegistration.js.map +1 -0
- package/dist/resolvers/peaksSidecar.d.ts +30 -0
- package/dist/resolvers/peaksSidecar.d.ts.map +1 -0
- package/dist/resolvers/peaksSidecar.js +51 -0
- package/dist/resolvers/peaksSidecar.js.map +1 -0
- package/dist/rest/MediaAccessKeys.d.ts +68 -0
- package/dist/rest/MediaAccessKeys.d.ts.map +1 -0
- package/dist/rest/MediaAccessKeys.js +96 -0
- package/dist/rest/MediaAccessKeys.js.map +1 -0
- package/dist/rest/MediaStreamHandler.d.ts +26 -0
- package/dist/rest/MediaStreamHandler.d.ts.map +1 -0
- package/dist/rest/MediaStreamHandler.js +192 -0
- package/dist/rest/MediaStreamHandler.js.map +1 -0
- package/dist/rest/mediaRange.d.ts +41 -0
- package/dist/rest/mediaRange.d.ts.map +1 -0
- package/dist/rest/mediaRange.js +60 -0
- package/dist/rest/mediaRange.js.map +1 -0
- package/package.json +83 -82
- package/src/__tests__/FileResolverPeaks.test.ts +64 -0
- package/src/__tests__/MediaAccessKeys.test.ts +68 -0
- package/src/__tests__/MediaStreamHandler.test.ts +91 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +10 -1
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +69 -0
- package/src/__tests__/RealtimeCoAgentAppAwareness.integration.test.ts +191 -0
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +123 -0
- package/src/__tests__/SessionManager.test.ts +2 -0
- package/src/__tests__/meetingRecordingRegistration.test.ts +248 -0
- package/src/agentSessions/SessionJanitor.ts +15 -1
- package/src/agentSessions/SessionManager.ts +62 -10
- package/src/agentSessions/index.ts +3 -1
- package/src/generated/generated.ts +17659 -13322
- package/src/index.ts +15 -1
- package/src/resolvers/ExecuteRemoteOperationResolver.ts +60 -4
- package/src/resolvers/FileResolver.ts +199 -1
- package/src/resolvers/IntegrationDiscoveryResolver.ts +108 -30
- package/src/resolvers/QueryResolver.ts +33 -19
- package/src/resolvers/RealtimeBridgeResolver.ts +82 -7
- package/src/resolvers/RealtimeClientSessionResolver.ts +326 -9
- package/src/resolvers/RemoteBrowserActionResolver.ts +18 -2
- package/src/resolvers/RunAIAgentResolver.ts +44 -11
- package/src/resolvers/RunTemplateResolver.ts +8 -0
- package/src/resolvers/meetingRecordingRegistration.ts +432 -0
- package/src/resolvers/peaksSidecar.ts +52 -0
- package/src/rest/MediaAccessKeys.ts +121 -0
- package/src/rest/MediaStreamHandler.ts +220 -0
- package/src/rest/mediaRange.ts +76 -0
|
@@ -116,6 +116,20 @@ export declare class CancelRealtimeSessionToolResult {
|
|
|
116
116
|
/** Human-readable failure reason. Null on success. */
|
|
117
117
|
ErrorMessage?: string;
|
|
118
118
|
}
|
|
119
|
+
/**
|
|
120
|
+
* Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
|
|
121
|
+
* success/failure envelope. A recording-storage failure must never throw to the browser (the
|
|
122
|
+
* session it belongs to has already ended), so problems come back as `Success: false` with a
|
|
123
|
+
* human-readable reason; only ownership/authn violations (from `loadOwnedSession`) still throw.
|
|
124
|
+
*/
|
|
125
|
+
export declare class UploadRealtimeRecordingResult {
|
|
126
|
+
/** True when the audio was stored and the session was stamped with the recording file. */
|
|
127
|
+
Success: boolean;
|
|
128
|
+
/** Human-readable failure reason. Null on success. */
|
|
129
|
+
ErrorMessage?: string;
|
|
130
|
+
/** ID of the created `MJ: Files` row holding the uploaded recording. Null on failure. */
|
|
131
|
+
FileID?: string;
|
|
132
|
+
}
|
|
119
133
|
/**
|
|
120
134
|
* Resolver for the client-direct realtime voice topology. A single {@link SessionManager} and a
|
|
121
135
|
* single {@link RealtimeClientSessionService} are shared across requests — neither holds per-user or
|
|
@@ -163,7 +177,7 @@ export declare class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
163
177
|
*
|
|
164
178
|
* @returns The ephemeral config + session linkage the browser needs to open its socket.
|
|
165
179
|
*/
|
|
166
|
-
StartRealtimeClientSession(targetAgentId: string | undefined, { userPayload, providers }: AppContext, conversationId?: string, lastSessionId?: string, preferredModelId?: string, clientToolsJson?: string, coAgentId?: string, configOverridesJson?: string): Promise<StartRealtimeClientSessionResult>;
|
|
180
|
+
StartRealtimeClientSession(targetAgentId: string | undefined, { userPayload, providers }: AppContext, conversationId?: string, lastSessionId?: string, preferredModelId?: string, clientToolsJson?: string, coAgentId?: string, configOverridesJson?: string, recordingStartedAt?: string, recordingConsent?: boolean, mediaCollectionId?: string, applicationId?: string, appContextJson?: string): Promise<StartRealtimeClientSessionResult>;
|
|
167
181
|
/**
|
|
168
182
|
* Execute a single tool call the browser relayed from its provider socket and return the
|
|
169
183
|
* serialized result for the browser to relay back to the model.
|
|
@@ -202,13 +216,53 @@ export declare class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
202
216
|
*
|
|
203
217
|
* @returns `true` when the transcript turn was persisted.
|
|
204
218
|
*/
|
|
205
|
-
RelayRealtimeTranscript(agentSessionId: string, role: string, text: string, { userPayload, providers }: AppContext, replacesPrevious?: boolean): Promise<boolean>;
|
|
219
|
+
RelayRealtimeTranscript(agentSessionId: string, role: string, text: string, { userPayload, providers }: AppContext, replacesPrevious?: boolean, utteranceStartMs?: number, utteranceEndMs?: number): Promise<boolean>;
|
|
206
220
|
/**
|
|
207
221
|
* Maps the relayed transcript role (`'User'`/`'AI'`/`'Assistant'`, case-insensitive) to the
|
|
208
222
|
* standard chat-message role stored in `AIPromptRun.Messages`. Anything that isn't an explicit
|
|
209
223
|
* user turn is treated as the assistant (the co-agent's own speech).
|
|
210
224
|
*/
|
|
211
225
|
private mapTranscriptRoleToChatRole;
|
|
226
|
+
/**
|
|
227
|
+
* Upload a CLIENT-DIRECT session audio recording, store it in MJStorage, link it to the owning
|
|
228
|
+
* `AIAgentSession`, and stamp the session's recording fields. The browser records locally during
|
|
229
|
+
* the call and uploads the assembled blob (base64) once the session ends.
|
|
230
|
+
*
|
|
231
|
+
* Hard gates (in order):
|
|
232
|
+
* 1. Ownership — `loadOwnedSession` enforces `UserID === contextUser.ID` (throws on violation).
|
|
233
|
+
* 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
|
|
234
|
+
* 3. Storage configuration — the session's agent must resolve a recording storage account (the
|
|
235
|
+
* agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
|
|
236
|
+
*
|
|
237
|
+
* Storage failures (and any unexpected throw) come back as `Success: false` with a reason — they
|
|
238
|
+
* NEVER throw to the browser, mirroring the shared {@link storeRealtimeRecording} contract.
|
|
239
|
+
*
|
|
240
|
+
* @param agentSessionId The session the recording belongs to (ownership-gated).
|
|
241
|
+
* @param audioBase64 The base64-encoded recording bytes (client-direct: seekable WAV/PCM).
|
|
242
|
+
* @param mimeType The audio MIME type (`audio/wav` for client-direct; legacy `audio/webm`/`ogg`/`mp4`).
|
|
243
|
+
* @param durationMs Optional client-measured recording duration (ms) — informational.
|
|
244
|
+
* @param consent Whether the user consented to recording. MUST be `true` or the upload is refused.
|
|
245
|
+
* @param peaks Optional capture-time waveform peaks (max-abs per bucket, normalized 0..1). When
|
|
246
|
+
* present, persisted as a `peaks.json` sidecar beside the recording for fast waveform rendering
|
|
247
|
+
* without re-decoding the audio.
|
|
248
|
+
* @returns A structured {@link UploadRealtimeRecordingResult} with the created `MJ: Files` id.
|
|
249
|
+
*/
|
|
250
|
+
UploadRealtimeRecording(agentSessionId: string, audioBase64: string, mimeType: string, ctx: AppContext, durationMs?: number, consent?: boolean, peaks?: number[]): Promise<UploadRealtimeRecordingResult>;
|
|
251
|
+
/**
|
|
252
|
+
* Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
|
|
253
|
+
* folder (`realtime-recordings/<sessionId>/seg-NNNN.<ext>`) as a raw storage object. Durability
|
|
254
|
+
* insurance during a live call so a browser/tab death loses at most the last window; the shards are
|
|
255
|
+
* deleted once the canonical consolidated file lands via {@link UploadRealtimeRecording}. Ownership-
|
|
256
|
+
* gated; consent was already established at session start. Best-effort — returns `false` (never
|
|
257
|
+
* throws) on any problem so a failed shard never disrupts the live call.
|
|
258
|
+
*
|
|
259
|
+
* @param agentSessionId The in-progress session (ownership-gated).
|
|
260
|
+
* @param segmentIndex 0-based shard index within the session.
|
|
261
|
+
* @param audioBase64 The base64-encoded shard bytes.
|
|
262
|
+
* @param mimeType The audio MIME type.
|
|
263
|
+
* @returns `true` when the shard was stored.
|
|
264
|
+
*/
|
|
265
|
+
UploadRealtimeRecordingSegment(agentSessionId: string, segmentIndex: number, audioBase64: string, mimeType: string, ctx: AppContext): Promise<boolean>;
|
|
212
266
|
/**
|
|
213
267
|
* Relays a co-agent CHANNEL tool-call (browser_ / Whiteboard_ etc.) onto the session's co-agent
|
|
214
268
|
* AIPromptRun.Messages — run-only observability so the run captures what the co-agent DID, not just
|
|
@@ -266,6 +320,12 @@ export declare class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
266
320
|
RelayRealtimeUsage(agentSessionId: string, inputTokens: number, outputTokens: number, { userPayload, providers }: AppContext): Promise<boolean>;
|
|
267
321
|
/** Clamps a relayed token delta: negative / non-finite values become 0. */
|
|
268
322
|
private clampTokenDelta;
|
|
323
|
+
/**
|
|
324
|
+
* Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
|
|
325
|
+
* finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
|
|
326
|
+
* the sidecar. Returns `undefined` for a missing/empty array (no sidecar written).
|
|
327
|
+
*/
|
|
328
|
+
private sanitizePeaks;
|
|
269
329
|
/**
|
|
270
330
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
271
331
|
* throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
|
|
@@ -643,6 +703,15 @@ export declare class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
643
703
|
* session-channel row is a silent no-op; failures log and never throw.
|
|
644
704
|
*/
|
|
645
705
|
private stampSessionChannelLastActive;
|
|
706
|
+
/**
|
|
707
|
+
* Tolerantly parses the `appContextJson` mutation arg into an {@link AppContextSnapshot}. Returns
|
|
708
|
+
* `undefined` for absent/blank/malformed/non-object payloads — app context is best-effort enrichment
|
|
709
|
+
* and must never fail a session start.
|
|
710
|
+
*
|
|
711
|
+
* @param appContextJson The raw JSON string the browser sent, or undefined.
|
|
712
|
+
* @returns The parsed snapshot, or `undefined`.
|
|
713
|
+
*/
|
|
714
|
+
private parseAppContext;
|
|
646
715
|
/**
|
|
647
716
|
* Tolerantly parses + validates client-declared UI tool definitions (see the SECURITY NOTE on
|
|
648
717
|
* {@link StartRealtimeClientSession}). Never throws — any rejection logs and returns
|
|
@@ -669,6 +738,16 @@ export declare class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
669
738
|
*
|
|
670
739
|
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
671
740
|
*/
|
|
741
|
+
/**
|
|
742
|
+
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
743
|
+
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
744
|
+
* is logged and swallowed — a recording-metadata problem must never fail the session start.
|
|
745
|
+
*
|
|
746
|
+
* @param session The freshly created/loaded session.
|
|
747
|
+
* @param recordingConsent Whether the user consented to recording — only `true` stamps anything.
|
|
748
|
+
* @param recordingStartedAt ISO-8601 recording start timestamp from the browser.
|
|
749
|
+
*/
|
|
750
|
+
private stampRecordingStart;
|
|
672
751
|
private persistTranscriptTurn;
|
|
673
752
|
/**
|
|
674
753
|
* CORRECTION persistence (the client transcript's `ReplacesPrevious` marker — e.g.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RealtimeClientSessionResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/RealtimeClientSessionResolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,OAAO,
|
|
1
|
+
{"version":3,"file":"RealtimeClientSessionResolver.d.ts","sourceRoot":"","sources":["../../src/resolvers/RealtimeClientSessionResolver.ts"],"names":[],"mappings":"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"}
|
|
@@ -36,12 +36,12 @@ var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
|
36
36
|
*
|
|
37
37
|
* @module @memberjunction/server
|
|
38
38
|
*/
|
|
39
|
-
import { Resolver, Mutation, Arg, Ctx, Int, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
39
|
+
import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
40
40
|
import { AuthorizationEvaluator, LogError, LogStatus, RunView } from '@memberjunction/core';
|
|
41
|
-
import { UUIDsEqual } from '@memberjunction/global';
|
|
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, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, } from '@memberjunction/ai-agents';
|
|
44
|
+
import { RealtimeClientSessionService, RealtimeChannelServerHost, EvaluateRuntimeOverrideAuthorization, ParseRealtimeTypeConfiguration, ResolveEffectiveRealtimeConfig, REALTIME_ADVANCED_SESSION_CONTROLS_AUTHORIZATION, resolveRecordingStorageAccountID, storeRealtimeRecording, writeRealtimeRecordingSegment, deleteRealtimeRecordingSegments, } from '@memberjunction/ai-agents';
|
|
45
45
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
46
46
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
47
47
|
import { GetReadWriteProvider } from '../util.js';
|
|
@@ -222,6 +222,30 @@ CancelRealtimeSessionToolResult = __decorate([
|
|
|
222
222
|
ObjectType()
|
|
223
223
|
], CancelRealtimeSessionToolResult);
|
|
224
224
|
export { CancelRealtimeSessionToolResult };
|
|
225
|
+
/**
|
|
226
|
+
* Result of {@link RealtimeClientSessionResolver.UploadRealtimeRecording} — a structured
|
|
227
|
+
* success/failure envelope. A recording-storage failure must never throw to the browser (the
|
|
228
|
+
* session it belongs to has already ended), so problems come back as `Success: false` with a
|
|
229
|
+
* human-readable reason; only ownership/authn violations (from `loadOwnedSession`) still throw.
|
|
230
|
+
*/
|
|
231
|
+
let UploadRealtimeRecordingResult = class UploadRealtimeRecordingResult {
|
|
232
|
+
};
|
|
233
|
+
__decorate([
|
|
234
|
+
Field(() => Boolean),
|
|
235
|
+
__metadata("design:type", Boolean)
|
|
236
|
+
], UploadRealtimeRecordingResult.prototype, "Success", void 0);
|
|
237
|
+
__decorate([
|
|
238
|
+
Field(() => String, { nullable: true }),
|
|
239
|
+
__metadata("design:type", String)
|
|
240
|
+
], UploadRealtimeRecordingResult.prototype, "ErrorMessage", void 0);
|
|
241
|
+
__decorate([
|
|
242
|
+
Field(() => String, { nullable: true }),
|
|
243
|
+
__metadata("design:type", String)
|
|
244
|
+
], UploadRealtimeRecordingResult.prototype, "FileID", void 0);
|
|
245
|
+
UploadRealtimeRecordingResult = __decorate([
|
|
246
|
+
ObjectType()
|
|
247
|
+
], UploadRealtimeRecordingResult);
|
|
248
|
+
export { UploadRealtimeRecordingResult };
|
|
225
249
|
/**
|
|
226
250
|
* Resolver for the client-direct realtime voice topology. A single {@link SessionManager} and a
|
|
227
251
|
* single {@link RealtimeClientSessionService} are shared across requests — neither holds per-user or
|
|
@@ -272,7 +296,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
272
296
|
*
|
|
273
297
|
* @returns The ephemeral config + session linkage the browser needs to open its socket.
|
|
274
298
|
*/
|
|
275
|
-
async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson) {
|
|
299
|
+
async StartRealtimeClientSession(targetAgentId, { userPayload, providers }, conversationId, lastSessionId, preferredModelId, clientToolsJson, coAgentId, configOverridesJson, recordingStartedAt, recordingConsent, mediaCollectionId, applicationId, appContextJson) {
|
|
276
300
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
277
301
|
const coAgentID = await this.resolveCoAgentID(targetAgentId, coAgentId, contextUser, provider);
|
|
278
302
|
// PAIRING CONSTRAINTS: a co-agent with pairing rows is restricted to that target list
|
|
@@ -285,6 +309,19 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
285
309
|
// silent ignore). Plain starts and within-pairing target selection are never gated here.
|
|
286
310
|
await this.assertRuntimeOverridesAuthorized(coAgentID, configOverridesJson, preferredModelId, contextUser, provider);
|
|
287
311
|
const config = { targetAgentID: effectiveTargetId };
|
|
312
|
+
// Per-session media-kit override: stored on the session config so the server-side MediaChannelServer
|
|
313
|
+
// resolves it (over the agent default) at start. Validate the id here — a malformed value is
|
|
314
|
+
// dropped (logged), never interpolated; the session still starts and falls back to the agent kit.
|
|
315
|
+
const mediaOverride = mediaCollectionId?.trim();
|
|
316
|
+
if (mediaOverride) {
|
|
317
|
+
if (IsValidUUID(mediaOverride)) {
|
|
318
|
+
config.mediaCollectionID = mediaOverride;
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
LogStatus(`StartRealtimeClientSession: ignoring malformed mediaCollectionId '${mediaCollectionId}' — ` +
|
|
322
|
+
'using the agent default media kit.');
|
|
323
|
+
}
|
|
324
|
+
}
|
|
288
325
|
const session = await this.sessionManager.CreateSession({
|
|
289
326
|
agentID: coAgentID,
|
|
290
327
|
userID: contextUser.ID,
|
|
@@ -292,12 +329,14 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
292
329
|
lastSessionID: lastSessionId,
|
|
293
330
|
config: JSON.stringify(config),
|
|
294
331
|
}, contextUser, provider);
|
|
332
|
+
// Best-effort: stamp recording-start metadata when the browser captured (with consent) at start.
|
|
333
|
+
await this.stampRecordingStart(session, recordingConsent, recordingStartedAt);
|
|
295
334
|
const clientTools = this.parseClientTools(clientToolsJson);
|
|
296
335
|
// Best-effort model-context hydration: the PRIOR session chain's transcript (ownership-
|
|
297
336
|
// checked, capped) is framed into the system prompt so the model REMEMBERS the last leg.
|
|
298
337
|
// Strictly tolerant — any problem yields no hydration, never a failed start.
|
|
299
338
|
const priorTranscript = await this.loadPriorTranscript(lastSessionId, contextUser, provider);
|
|
300
|
-
const result = await this.prepareClientSessionOrClose(session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson);
|
|
339
|
+
const result = await this.prepareClientSessionOrClose(session, coAgentID, effectiveTargetId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson, applicationId, this.parseAppContext(appContextJson));
|
|
301
340
|
// Best-effort restore of the PRIOR session's persisted channel states (e.g. the whiteboard
|
|
302
341
|
// board). Strictly tolerant — any problem yields a null field, never a failed start.
|
|
303
342
|
result.PriorChannelStatesJson = (await this.loadPriorChannelStatesJson(lastSessionId, contextUser, provider)) ?? undefined;
|
|
@@ -319,6 +358,9 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
319
358
|
const { ResultJson, PausedRunID, Artifacts } = await this.clientSessionService.ExecuteRelayedTool({
|
|
320
359
|
AgentSessionID: agentSessionId,
|
|
321
360
|
TargetAgentID: config.targetAgentID,
|
|
361
|
+
// Multi-target (Move 4): the session's persisted allowed-agent union — a model-named
|
|
362
|
+
// colleague in the call is validated against this; absent ⇒ single-target behavior.
|
|
363
|
+
AllowedAgents: config.allowedAgents,
|
|
322
364
|
// Nest the delegated target-agent run under the co-agent observability run (when present).
|
|
323
365
|
ParentRunID: config.coAgentRunID,
|
|
324
366
|
Call: { CallID: callId, ToolName: toolName, Arguments: argsJson },
|
|
@@ -378,6 +420,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
378
420
|
promptRunID: config.promptRunID,
|
|
379
421
|
coAgentRunStepID: config.coAgentRunStepID,
|
|
380
422
|
pendingFeedbackRunID: pausedRunID,
|
|
423
|
+
applicationID: config.applicationID,
|
|
424
|
+
allowedAgents: config.allowedAgents,
|
|
381
425
|
};
|
|
382
426
|
session.Config_ = JSON.stringify(next);
|
|
383
427
|
if (!(await session.Save())) {
|
|
@@ -397,12 +441,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
397
441
|
*
|
|
398
442
|
* @returns `true` when the transcript turn was persisted.
|
|
399
443
|
*/
|
|
400
|
-
async RelayRealtimeTranscript(agentSessionId, role, text, { userPayload, providers }, replacesPrevious) {
|
|
444
|
+
async RelayRealtimeTranscript(agentSessionId, role, text, { userPayload, providers }, replacesPrevious, utteranceStartMs, utteranceEndMs) {
|
|
401
445
|
const { contextUser, provider } = this.requireUserAndProvider(userPayload, providers);
|
|
402
446
|
const session = await this.loadOwnedActiveSession(agentSessionId, contextUser, provider);
|
|
403
447
|
const saved = replacesPrevious
|
|
404
|
-
? await this.replacePreviousTranscriptTurn(session, role, text, contextUser, provider)
|
|
405
|
-
: await this.persistTranscriptTurn(session, role, text, contextUser, provider);
|
|
448
|
+
? await this.replacePreviousTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs)
|
|
449
|
+
: await this.persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs);
|
|
406
450
|
if (!saved) {
|
|
407
451
|
return false;
|
|
408
452
|
}
|
|
@@ -423,6 +467,122 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
423
467
|
mapTranscriptRoleToChatRole(role) {
|
|
424
468
|
return role.trim().toLowerCase() === 'user' ? 'user' : 'assistant';
|
|
425
469
|
}
|
|
470
|
+
/**
|
|
471
|
+
* Upload a CLIENT-DIRECT session audio recording, store it in MJStorage, link it to the owning
|
|
472
|
+
* `AIAgentSession`, and stamp the session's recording fields. The browser records locally during
|
|
473
|
+
* the call and uploads the assembled blob (base64) once the session ends.
|
|
474
|
+
*
|
|
475
|
+
* Hard gates (in order):
|
|
476
|
+
* 1. Ownership — `loadOwnedSession` enforces `UserID === contextUser.ID` (throws on violation).
|
|
477
|
+
* 2. Consent — `consent !== true` is a hard refusal: no audio is ever stored without it.
|
|
478
|
+
* 3. Storage configuration — the session's agent must resolve a recording storage account (the
|
|
479
|
+
* agent's `RecordingStorageProviderID`, else `AttachmentStorageProviderID`).
|
|
480
|
+
*
|
|
481
|
+
* Storage failures (and any unexpected throw) come back as `Success: false` with a reason — they
|
|
482
|
+
* NEVER throw to the browser, mirroring the shared {@link storeRealtimeRecording} contract.
|
|
483
|
+
*
|
|
484
|
+
* @param agentSessionId The session the recording belongs to (ownership-gated).
|
|
485
|
+
* @param audioBase64 The base64-encoded recording bytes (client-direct: seekable WAV/PCM).
|
|
486
|
+
* @param mimeType The audio MIME type (`audio/wav` for client-direct; legacy `audio/webm`/`ogg`/`mp4`).
|
|
487
|
+
* @param durationMs Optional client-measured recording duration (ms) — informational.
|
|
488
|
+
* @param consent Whether the user consented to recording. MUST be `true` or the upload is refused.
|
|
489
|
+
* @param peaks Optional capture-time waveform peaks (max-abs per bucket, normalized 0..1). When
|
|
490
|
+
* present, persisted as a `peaks.json` sidecar beside the recording for fast waveform rendering
|
|
491
|
+
* without re-decoding the audio.
|
|
492
|
+
* @returns A structured {@link UploadRealtimeRecordingResult} with the created `MJ: Files` id.
|
|
493
|
+
*/
|
|
494
|
+
async UploadRealtimeRecording(agentSessionId, audioBase64, mimeType, ctx, durationMs, consent, peaks) {
|
|
495
|
+
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
496
|
+
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
497
|
+
// HARD consent gate — no audio is ever stored without an explicit grant.
|
|
498
|
+
if (consent !== true) {
|
|
499
|
+
return { Success: false, ErrorMessage: 'Recording consent was not granted.' };
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const agent = await provider.GetEntityObject('MJ: AI Agents', contextUser);
|
|
503
|
+
if (!(await agent.Load(session.AgentID))) {
|
|
504
|
+
return { Success: false, ErrorMessage: `Co-agent ${session.AgentID} for the session could not be loaded.` };
|
|
505
|
+
}
|
|
506
|
+
const accountID = await resolveRecordingStorageAccountID(agent, contextUser, provider);
|
|
507
|
+
if (!accountID) {
|
|
508
|
+
return { Success: false, ErrorMessage: 'No recording storage account is configured for this agent.' };
|
|
509
|
+
}
|
|
510
|
+
const buffer = Buffer.from(audioBase64, 'base64');
|
|
511
|
+
if (buffer.length === 0) {
|
|
512
|
+
return { Success: false, ErrorMessage: 'The uploaded recording was empty.' };
|
|
513
|
+
}
|
|
514
|
+
const fileID = await storeRealtimeRecording({
|
|
515
|
+
Audio: buffer,
|
|
516
|
+
MimeType: mimeType,
|
|
517
|
+
Media: 'Audio',
|
|
518
|
+
StartedAt: session.RecordingStartedAt ?? new Date(),
|
|
519
|
+
StorageAccountID: accountID,
|
|
520
|
+
SessionID: agentSessionId,
|
|
521
|
+
ContextUser: contextUser,
|
|
522
|
+
Provider: provider,
|
|
523
|
+
// Sanitized capture-time waveform peaks → persisted as a peaks.json sidecar.
|
|
524
|
+
Peaks: this.sanitizePeaks(peaks),
|
|
525
|
+
});
|
|
526
|
+
// Canonical consolidated file written — drop the crash-recovery shards (best-effort).
|
|
527
|
+
if (fileID) {
|
|
528
|
+
await deleteRealtimeRecordingSegments(agentSessionId, accountID, contextUser);
|
|
529
|
+
}
|
|
530
|
+
return {
|
|
531
|
+
Success: !!fileID,
|
|
532
|
+
FileID: fileID ?? undefined,
|
|
533
|
+
ErrorMessage: fileID ? undefined : 'Storage upload failed.',
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
catch (error) {
|
|
537
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
538
|
+
LogError(`RealtimeClientSessionResolver.UploadRealtimeRecording failed for session ${agentSessionId}: ${message}`);
|
|
539
|
+
return { Success: false, ErrorMessage: message };
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
/**
|
|
543
|
+
* Uploads ONE crash-recovery audio shard (~15s) for an IN-PROGRESS recording into the session's
|
|
544
|
+
* folder (`realtime-recordings/<sessionId>/seg-NNNN.<ext>`) as a raw storage object. Durability
|
|
545
|
+
* insurance during a live call so a browser/tab death loses at most the last window; the shards are
|
|
546
|
+
* deleted once the canonical consolidated file lands via {@link UploadRealtimeRecording}. Ownership-
|
|
547
|
+
* gated; consent was already established at session start. Best-effort — returns `false` (never
|
|
548
|
+
* throws) on any problem so a failed shard never disrupts the live call.
|
|
549
|
+
*
|
|
550
|
+
* @param agentSessionId The in-progress session (ownership-gated).
|
|
551
|
+
* @param segmentIndex 0-based shard index within the session.
|
|
552
|
+
* @param audioBase64 The base64-encoded shard bytes.
|
|
553
|
+
* @param mimeType The audio MIME type.
|
|
554
|
+
* @returns `true` when the shard was stored.
|
|
555
|
+
*/
|
|
556
|
+
async UploadRealtimeRecordingSegment(agentSessionId, segmentIndex, audioBase64, mimeType, ctx) {
|
|
557
|
+
try {
|
|
558
|
+
const { contextUser, provider } = this.requireUserAndProvider(ctx.userPayload, ctx.providers);
|
|
559
|
+
const session = await this.loadOwnedSession(agentSessionId, contextUser, provider);
|
|
560
|
+
const agent = await provider.GetEntityObject('MJ: AI Agents', contextUser);
|
|
561
|
+
if (!(await agent.Load(session.AgentID))) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
564
|
+
const accountID = await resolveRecordingStorageAccountID(agent, contextUser, provider);
|
|
565
|
+
if (!accountID) {
|
|
566
|
+
return false;
|
|
567
|
+
}
|
|
568
|
+
const buffer = Buffer.from(audioBase64, 'base64');
|
|
569
|
+
if (buffer.length === 0) {
|
|
570
|
+
return false;
|
|
571
|
+
}
|
|
572
|
+
return await writeRealtimeRecordingSegment({
|
|
573
|
+
SessionID: agentSessionId,
|
|
574
|
+
SegmentIndex: segmentIndex,
|
|
575
|
+
Audio: buffer,
|
|
576
|
+
MimeType: mimeType,
|
|
577
|
+
StorageAccountID: accountID,
|
|
578
|
+
ContextUser: contextUser,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
catch (error) {
|
|
582
|
+
LogError(`RealtimeClientSessionResolver.UploadRealtimeRecordingSegment failed for session ${agentSessionId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
426
586
|
/**
|
|
427
587
|
* Relays a co-agent CHANNEL tool-call (browser_ / Whiteboard_ etc.) onto the session's co-agent
|
|
428
588
|
* AIPromptRun.Messages — run-only observability so the run captures what the co-agent DID, not just
|
|
@@ -525,6 +685,22 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
525
685
|
clampTokenDelta(value) {
|
|
526
686
|
return Number.isFinite(value) && value > 0 ? Math.floor(value) : 0;
|
|
527
687
|
}
|
|
688
|
+
/**
|
|
689
|
+
* Sanitizes client-supplied waveform peaks before they're persisted: drops anything that isn't a
|
|
690
|
+
* finite number, clamps each to `[0, 1]`, and caps the array length so a hostile client can't bloat
|
|
691
|
+
* the sidecar. Returns `undefined` for a missing/empty array (no sidecar written).
|
|
692
|
+
*/
|
|
693
|
+
sanitizePeaks(peaks) {
|
|
694
|
+
if (!Array.isArray(peaks) || peaks.length === 0) {
|
|
695
|
+
return undefined;
|
|
696
|
+
}
|
|
697
|
+
const MAX_PEAKS = 4096; // generous ceiling above the client's ~600-bucket target
|
|
698
|
+
const cleaned = peaks
|
|
699
|
+
.slice(0, MAX_PEAKS)
|
|
700
|
+
.filter((v) => Number.isFinite(v))
|
|
701
|
+
.map((v) => (v < 0 ? 0 : v > 1 ? 1 : v));
|
|
702
|
+
return cleaned.length > 0 ? cleaned : undefined;
|
|
703
|
+
}
|
|
528
704
|
/**
|
|
529
705
|
* Reads the co-agent `AIPromptRun` id from the session config WITHOUT the relay path's
|
|
530
706
|
* throw-on-missing-target semantics — usage relay is best-effort, so a missing/malformed
|
|
@@ -1014,7 +1190,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1014
1190
|
* (from {@link loadPriorTranscript}) — the service frames it into the system prompt so a
|
|
1015
1191
|
* resumed session remembers the previous leg(s).
|
|
1016
1192
|
*/
|
|
1017
|
-
async prepareClientSessionOrClose(session, coAgentID, targetAgentId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson) {
|
|
1193
|
+
async prepareClientSessionOrClose(session, coAgentID, targetAgentId, contextUser, provider, preferredModelId, clientTools, priorTranscript, configOverridesJson, applicationId, appContext) {
|
|
1018
1194
|
const prep = await this.clientSessionService.PrepareClientSession({
|
|
1019
1195
|
CoAgentID: coAgentID,
|
|
1020
1196
|
TargetAgentID: targetAgentId,
|
|
@@ -1033,13 +1209,17 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1033
1209
|
PriorTranscript: priorTranscript,
|
|
1034
1210
|
// Pre-authorized runtime override layer (assertRuntimeOverridesAuthorized gated it).
|
|
1035
1211
|
ConfigOverridesJson: configOverridesJson,
|
|
1212
|
+
// App awareness (Move 1/3/4): the app sources the app cascade layer + RelevantAgents
|
|
1213
|
+
// (allowed-agent union); the snapshot is injected into the system prompt at mint.
|
|
1214
|
+
ApplicationID: applicationId,
|
|
1215
|
+
AppContext: appContext,
|
|
1036
1216
|
}, contextUser, provider);
|
|
1037
1217
|
if (!prep.Success || !prep.ClientConfig) {
|
|
1038
1218
|
// Prep failure is an ERROR close, not an explicit user hang-up — stamp it as such.
|
|
1039
1219
|
await this.sessionManager.CloseSession(session.ID, contextUser, provider, 'Error');
|
|
1040
1220
|
throw new Error(prep.ErrorMessage ?? 'Failed to prepare the client realtime session.');
|
|
1041
1221
|
}
|
|
1042
|
-
await this.persistObservabilityRunIDs(session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID);
|
|
1222
|
+
await this.persistObservabilityRunIDs(session, targetAgentId, prep.CoAgentRunID, prep.PromptRunID, prep.CoAgentRunStepID, applicationId, prep.EffectiveConfig?.realtime?.allowedAgents);
|
|
1043
1223
|
const cfg = prep.ClientConfig;
|
|
1044
1224
|
return {
|
|
1045
1225
|
AgentSessionId: session.ID,
|
|
@@ -1061,8 +1241,11 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1061
1241
|
* finalize the runs (and on relay to nest delegated runs). Best-effort: a save failure is logged,
|
|
1062
1242
|
* not thrown — the voice session still proceeds, it just won't carry the run ids.
|
|
1063
1243
|
*/
|
|
1064
|
-
async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID) {
|
|
1065
|
-
const config = {
|
|
1244
|
+
async persistObservabilityRunIDs(session, targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID, allowedAgents) {
|
|
1245
|
+
const config = {
|
|
1246
|
+
targetAgentID, coAgentRunID, promptRunID, coAgentRunStepID, applicationID,
|
|
1247
|
+
allowedAgents: allowedAgents && allowedAgents.length > 0 ? allowedAgents : undefined,
|
|
1248
|
+
};
|
|
1066
1249
|
session.Config_ = JSON.stringify(config);
|
|
1067
1250
|
const saved = await session.Save();
|
|
1068
1251
|
if (!saved) {
|
|
@@ -1545,6 +1728,28 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1545
1728
|
LogError(`SaveSessionChannelArtifact: LastActiveAt stamp failed: ${error.message}`);
|
|
1546
1729
|
}
|
|
1547
1730
|
}
|
|
1731
|
+
/**
|
|
1732
|
+
* Tolerantly parses the `appContextJson` mutation arg into an {@link AppContextSnapshot}. Returns
|
|
1733
|
+
* `undefined` for absent/blank/malformed/non-object payloads — app context is best-effort enrichment
|
|
1734
|
+
* and must never fail a session start.
|
|
1735
|
+
*
|
|
1736
|
+
* @param appContextJson The raw JSON string the browser sent, or undefined.
|
|
1737
|
+
* @returns The parsed snapshot, or `undefined`.
|
|
1738
|
+
*/
|
|
1739
|
+
parseAppContext(appContextJson) {
|
|
1740
|
+
if (!appContextJson || !appContextJson.trim()) {
|
|
1741
|
+
return undefined;
|
|
1742
|
+
}
|
|
1743
|
+
try {
|
|
1744
|
+
const parsed = JSON.parse(appContextJson);
|
|
1745
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
1746
|
+
? parsed
|
|
1747
|
+
: undefined;
|
|
1748
|
+
}
|
|
1749
|
+
catch {
|
|
1750
|
+
return undefined;
|
|
1751
|
+
}
|
|
1752
|
+
}
|
|
1548
1753
|
/**
|
|
1549
1754
|
* Tolerantly parses + validates client-declared UI tool definitions (see the SECURITY NOTE on
|
|
1550
1755
|
* {@link StartRealtimeClientSession}). Never throws — any rejection logs and returns
|
|
@@ -1613,6 +1818,8 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1613
1818
|
promptRunID: typeof parsed.promptRunID === 'string' ? parsed.promptRunID : undefined,
|
|
1614
1819
|
coAgentRunStepID: typeof parsed.coAgentRunStepID === 'string' ? parsed.coAgentRunStepID : undefined,
|
|
1615
1820
|
pendingFeedbackRunID: typeof parsed.pendingFeedbackRunID === 'string' ? parsed.pendingFeedbackRunID : undefined,
|
|
1821
|
+
applicationID: typeof parsed.applicationID === 'string' ? parsed.applicationID : undefined,
|
|
1822
|
+
allowedAgents: Array.isArray(parsed.allowedAgents) ? parsed.allowedAgents : undefined,
|
|
1616
1823
|
};
|
|
1617
1824
|
}
|
|
1618
1825
|
}
|
|
@@ -1628,7 +1835,31 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1628
1835
|
*
|
|
1629
1836
|
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
1630
1837
|
*/
|
|
1631
|
-
|
|
1838
|
+
/**
|
|
1839
|
+
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
1840
|
+
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
1841
|
+
* is logged and swallowed — a recording-metadata problem must never fail the session start.
|
|
1842
|
+
*
|
|
1843
|
+
* @param session The freshly created/loaded session.
|
|
1844
|
+
* @param recordingConsent Whether the user consented to recording — only `true` stamps anything.
|
|
1845
|
+
* @param recordingStartedAt ISO-8601 recording start timestamp from the browser.
|
|
1846
|
+
*/
|
|
1847
|
+
async stampRecordingStart(session, recordingConsent, recordingStartedAt) {
|
|
1848
|
+
if (recordingConsent !== true || !recordingStartedAt) {
|
|
1849
|
+
return;
|
|
1850
|
+
}
|
|
1851
|
+
const startedAt = new Date(recordingStartedAt);
|
|
1852
|
+
if (Number.isNaN(startedAt.getTime())) {
|
|
1853
|
+
LogError(`RealtimeClientSessionResolver.stampRecordingStart: invalid recordingStartedAt '${recordingStartedAt}' for session ${session.ID}`);
|
|
1854
|
+
return;
|
|
1855
|
+
}
|
|
1856
|
+
session.RecordingStartedAt = startedAt;
|
|
1857
|
+
session.RecordingMedia = 'Audio';
|
|
1858
|
+
if (!(await session.Save())) {
|
|
1859
|
+
LogError(`RealtimeClientSessionResolver.stampRecordingStart save failed: ${session.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
async persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
|
|
1632
1863
|
const detail = await provider.GetEntityObject(CONVERSATION_DETAIL_ENTITY, contextUser);
|
|
1633
1864
|
detail.NewRecord();
|
|
1634
1865
|
detail.ConversationID = session.ConversationID;
|
|
@@ -1636,6 +1867,12 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1636
1867
|
detail.Message = text;
|
|
1637
1868
|
detail.AgentSessionID = session.ID;
|
|
1638
1869
|
detail.UserID = contextUser.ID;
|
|
1870
|
+
if (typeof utteranceStartMs === 'number' && utteranceStartMs >= 0) {
|
|
1871
|
+
detail.UtteranceStartMs = utteranceStartMs;
|
|
1872
|
+
}
|
|
1873
|
+
if (typeof utteranceEndMs === 'number' && utteranceEndMs >= 0) {
|
|
1874
|
+
detail.UtteranceEndMs = utteranceEndMs;
|
|
1875
|
+
}
|
|
1639
1876
|
const saved = await detail.Save();
|
|
1640
1877
|
if (!saved) {
|
|
1641
1878
|
LogError(`RealtimeClientSessionResolver.persistTranscriptTurn save failed: ${detail.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
@@ -1649,7 +1886,7 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1649
1886
|
* appending a near-duplicate. Falls back to a plain insert when no prior turn exists
|
|
1650
1887
|
* (the superseded turn may have failed to persist) — a correction is never dropped.
|
|
1651
1888
|
*/
|
|
1652
|
-
async replacePreviousTranscriptTurn(session, role, text, contextUser, provider) {
|
|
1889
|
+
async replacePreviousTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs) {
|
|
1653
1890
|
const mappedRole = this.mapTranscriptRole(role);
|
|
1654
1891
|
const rv = RunView.FromMetadataProvider(provider);
|
|
1655
1892
|
const result = await rv.RunView({
|
|
@@ -1661,9 +1898,17 @@ let RealtimeClientSessionResolver = class RealtimeClientSessionResolver extends
|
|
|
1661
1898
|
}, contextUser);
|
|
1662
1899
|
const previous = result.Success ? (result.Results?.[0] ?? null) : null;
|
|
1663
1900
|
if (!previous) {
|
|
1664
|
-
return this.persistTranscriptTurn(session, role, text, contextUser, provider);
|
|
1901
|
+
return this.persistTranscriptTurn(session, role, text, contextUser, provider, utteranceStartMs, utteranceEndMs);
|
|
1665
1902
|
}
|
|
1666
1903
|
previous.Message = text;
|
|
1904
|
+
// The correction extends the existing turn: always refresh the end boundary when provided,
|
|
1905
|
+
// but only set the start when it wasn't already captured on the superseded turn.
|
|
1906
|
+
if (typeof utteranceEndMs === 'number' && utteranceEndMs >= 0) {
|
|
1907
|
+
previous.UtteranceEndMs = utteranceEndMs;
|
|
1908
|
+
}
|
|
1909
|
+
if (typeof utteranceStartMs === 'number' && utteranceStartMs >= 0 && previous.UtteranceStartMs == null) {
|
|
1910
|
+
previous.UtteranceStartMs = utteranceStartMs;
|
|
1911
|
+
}
|
|
1667
1912
|
const saved = await previous.Save();
|
|
1668
1913
|
if (!saved) {
|
|
1669
1914
|
LogError(`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed: ${previous.LatestResult?.CompleteMessage ?? 'unknown error'}`);
|
|
@@ -1689,8 +1934,13 @@ __decorate([
|
|
|
1689
1934
|
__param(5, Arg('clientToolsJson', () => String, { nullable: true })),
|
|
1690
1935
|
__param(6, Arg('coAgentId', () => String, { nullable: true })),
|
|
1691
1936
|
__param(7, Arg('configOverridesJson', () => String, { nullable: true })),
|
|
1937
|
+
__param(8, Arg('recordingStartedAt', () => String, { nullable: true })),
|
|
1938
|
+
__param(9, Arg('recordingConsent', () => Boolean, { nullable: true })),
|
|
1939
|
+
__param(10, Arg('mediaCollectionId', () => String, { nullable: true })),
|
|
1940
|
+
__param(11, Arg('applicationId', () => String, { nullable: true })),
|
|
1941
|
+
__param(12, Arg('appContextJson', () => String, { nullable: true })),
|
|
1692
1942
|
__metadata("design:type", Function),
|
|
1693
|
-
__metadata("design:paramtypes", [String, Object, String, String, String, String, String, String]),
|
|
1943
|
+
__metadata("design:paramtypes", [String, Object, String, String, String, String, String, String, String, Boolean, String, String, String]),
|
|
1694
1944
|
__metadata("design:returntype", Promise)
|
|
1695
1945
|
], RealtimeClientSessionResolver.prototype, "StartRealtimeClientSession", null);
|
|
1696
1946
|
__decorate([
|
|
@@ -1712,10 +1962,36 @@ __decorate([
|
|
|
1712
1962
|
__param(2, Arg('text', () => String)),
|
|
1713
1963
|
__param(3, Ctx()),
|
|
1714
1964
|
__param(4, Arg('replacesPrevious', () => Boolean, { nullable: true })),
|
|
1965
|
+
__param(5, Arg('utteranceStartMs', () => Int, { nullable: true })),
|
|
1966
|
+
__param(6, Arg('utteranceEndMs', () => Int, { nullable: true })),
|
|
1715
1967
|
__metadata("design:type", Function),
|
|
1716
|
-
__metadata("design:paramtypes", [String, String, String, Object, Boolean]),
|
|
1968
|
+
__metadata("design:paramtypes", [String, String, String, Object, Boolean, Number, Number]),
|
|
1717
1969
|
__metadata("design:returntype", Promise)
|
|
1718
1970
|
], RealtimeClientSessionResolver.prototype, "RelayRealtimeTranscript", null);
|
|
1971
|
+
__decorate([
|
|
1972
|
+
Mutation(() => UploadRealtimeRecordingResult),
|
|
1973
|
+
__param(0, Arg('agentSessionId', () => String)),
|
|
1974
|
+
__param(1, Arg('audioBase64', () => String)),
|
|
1975
|
+
__param(2, Arg('mimeType', () => String)),
|
|
1976
|
+
__param(3, Ctx()),
|
|
1977
|
+
__param(4, Arg('durationMs', () => Int, { nullable: true })),
|
|
1978
|
+
__param(5, Arg('consent', () => Boolean, { nullable: true })),
|
|
1979
|
+
__param(6, Arg('peaks', () => [Float], { nullable: true })),
|
|
1980
|
+
__metadata("design:type", Function),
|
|
1981
|
+
__metadata("design:paramtypes", [String, String, String, Object, Number, Boolean, Array]),
|
|
1982
|
+
__metadata("design:returntype", Promise)
|
|
1983
|
+
], RealtimeClientSessionResolver.prototype, "UploadRealtimeRecording", null);
|
|
1984
|
+
__decorate([
|
|
1985
|
+
Mutation(() => Boolean),
|
|
1986
|
+
__param(0, Arg('agentSessionId', () => String)),
|
|
1987
|
+
__param(1, Arg('segmentIndex', () => Int)),
|
|
1988
|
+
__param(2, Arg('audioBase64', () => String)),
|
|
1989
|
+
__param(3, Arg('mimeType', () => String)),
|
|
1990
|
+
__param(4, Ctx()),
|
|
1991
|
+
__metadata("design:type", Function),
|
|
1992
|
+
__metadata("design:paramtypes", [String, Number, String, String, Object]),
|
|
1993
|
+
__metadata("design:returntype", Promise)
|
|
1994
|
+
], RealtimeClientSessionResolver.prototype, "UploadRealtimeRecordingSegment", null);
|
|
1719
1995
|
__decorate([
|
|
1720
1996
|
Mutation(() => Boolean),
|
|
1721
1997
|
__param(0, Arg('agentSessionId', () => String)),
|