@memberjunction/server 6.1.3 → 6.1.5
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/auth/magicLink/MagicLinkService.d.ts +17 -15
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +49 -35
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +20 -52
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +23 -0
- 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/generic/CacheInvalidationResolver.d.ts +87 -3
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +118 -3
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/DeleteOptionsInput.d.ts +8 -1
- package/dist/generic/DeleteOptionsInput.d.ts.map +1 -1
- package/dist/generic/DeleteOptionsInput.js +1 -1
- package/dist/generic/DeleteOptionsInput.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +4 -0
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +10 -3
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +49 -47
- 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 +10 -20
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +54 -174
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +44 -6
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +90 -34
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +2 -1
- package/dist/resolvers/RunAIAgentResolver.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/services/TaskGraphActionRunner.d.ts +13 -8
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +16 -15
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +12 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/DeleteOptionsInput.wireShape.test.ts +62 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +129 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +177 -0
- package/src/__tests__/magicLink.test.ts +15 -76
- package/src/__tests__/magicLinkService.consume.test.ts +224 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/magicLink/MagicLinkService.ts +55 -36
- package/src/auth/magicLink/magicLinkCore.ts +19 -56
- package/src/config.ts +23 -0
- package/src/context.ts +11 -24
- package/src/generic/CacheInvalidationResolver.ts +139 -3
- package/src/generic/DeleteOptionsInput.ts +9 -2
- package/src/generic/ResolverBase.ts +11 -3
- package/src/index.ts +55 -48
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +64 -194
- package/src/resolvers/RealtimeClientSessionResolver.ts +93 -35
- package/src/resolvers/RunAIAgentResolver.ts +4 -1
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/services/TaskGraphActionRunner.ts +16 -15
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -42
- 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 -50
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
import { Resolver, Mutation, Arg, Ctx, Int, Float, ObjectType, Field, PubSub, PubSubEngine } from 'type-graphql';
|
|
28
28
|
import { AppContext, UserPayload } from '../types.js';
|
|
29
|
-
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView } from '@memberjunction/core';
|
|
29
|
+
import { AuthorizationEvaluator, UserInfo, IMetadataProvider, LogError, LogStatus, RunView, BaseEntity } from '@memberjunction/core';
|
|
30
30
|
import { UUIDsEqual, IsValidUUID } from '@memberjunction/global';
|
|
31
31
|
import {
|
|
32
32
|
MJAIAgentEntity,
|
|
@@ -376,6 +376,31 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
376
376
|
private readonly clientSessionService = new RealtimeClientSessionService();
|
|
377
377
|
private readonly sessionManager = new SessionManager(this.clientSessionService);
|
|
378
378
|
|
|
379
|
+
/**
|
|
380
|
+
* The reason a `Save()`/`Delete()` returned false, for a log line. `CompleteMessage` renders
|
|
381
|
+
* `Message`, `Error` and every `Errors[]` entry (validation errors included, since `Save()`
|
|
382
|
+
* runs `Validate()` and records its errors there), so there is no separate `Validate()` call.
|
|
383
|
+
* When a subclass refused the write without recording anything, say so explicitly instead of
|
|
384
|
+
* "unknown error", which is what hid MJ#4791.
|
|
385
|
+
*
|
|
386
|
+
* Only a NULL `LatestResult` proves the refusal happened before the provider (nothing was
|
|
387
|
+
* registered at all). A SUCCESS `LatestResult` is an earlier save on the same instance — the
|
|
388
|
+
* refusal registered nothing — so its text must never be reported as this failure's reason.
|
|
389
|
+
*/
|
|
390
|
+
private describeSaveFailure(entity: BaseEntity): string {
|
|
391
|
+
const latest = entity.LatestResult;
|
|
392
|
+
if (!latest) {
|
|
393
|
+
return `no failure detail recorded (LatestResult null; ResultHistory length ${entity.ResultHistory.length}) ` +
|
|
394
|
+
'— the write was refused before reaching the provider';
|
|
395
|
+
}
|
|
396
|
+
if (latest.Success) {
|
|
397
|
+
return 'no failure detail recorded (latest result entry is a prior success; ' +
|
|
398
|
+
`ResultHistory length ${entity.ResultHistory.length})`;
|
|
399
|
+
}
|
|
400
|
+
const detail = latest.CompleteMessage?.trim();
|
|
401
|
+
return detail || `no failure detail recorded (LatestResult empty; ResultHistory length ${entity.ResultHistory.length})`;
|
|
402
|
+
}
|
|
403
|
+
|
|
379
404
|
/**
|
|
380
405
|
* Start a client-direct realtime voice session targeting `targetAgentId`.
|
|
381
406
|
*
|
|
@@ -581,7 +606,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
581
606
|
// Junction-link any artifacts the delegated run produced into the session's conversation
|
|
582
607
|
// history (best-effort) — so chat, session review, and resume carryover can all see them.
|
|
583
608
|
// Runs as `runUser`: the junction entity is not among an anonymous caller's relay grants.
|
|
584
|
-
|
|
609
|
+
// The hidden anchor detail it may need to create, though, is written by the CALLER (#4791)
|
|
610
|
+
// — same reason `persistDirectActionTurn` is: the System user is not the conversation owner.
|
|
611
|
+
await this.linkDelegatedArtifactsToConversation(session, Artifacts, runUser, contextUser, provider);
|
|
585
612
|
|
|
586
613
|
await this.sessionManager.Heartbeat(agentSessionId, contextUser, provider);
|
|
587
614
|
return ResultJson;
|
|
@@ -645,7 +672,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
645
672
|
session.Config_ = JSON.stringify(next);
|
|
646
673
|
if (!(await session.Save())) {
|
|
647
674
|
LogError(
|
|
648
|
-
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed
|
|
675
|
+
`RealtimeClientSessionResolver.updatePendingFeedbackRunID save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
649
676
|
);
|
|
650
677
|
}
|
|
651
678
|
}
|
|
@@ -1797,7 +1824,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1797
1824
|
const saved = await session.Save();
|
|
1798
1825
|
if (!saved) {
|
|
1799
1826
|
LogError(
|
|
1800
|
-
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed
|
|
1827
|
+
`RealtimeClientSessionResolver.persistObservabilityRunIDs save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
1801
1828
|
);
|
|
1802
1829
|
}
|
|
1803
1830
|
}
|
|
@@ -1915,7 +1942,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
1915
1942
|
if (!saved) {
|
|
1916
1943
|
LogError(
|
|
1917
1944
|
`SaveSessionChannelState: save failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
1918
|
-
`${row
|
|
1945
|
+
`${this.describeSaveFailure(row)}`,
|
|
1919
1946
|
);
|
|
1920
1947
|
}
|
|
1921
1948
|
return saved;
|
|
@@ -2200,7 +2227,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2200
2227
|
artifact.UserID = contextUser.ID;
|
|
2201
2228
|
artifact.Visibility = 'Always';
|
|
2202
2229
|
if (!(await artifact.Save())) {
|
|
2203
|
-
const message = `SaveSessionChannelArtifact: artifact save failed: ${artifact
|
|
2230
|
+
const message = `SaveSessionChannelArtifact: artifact save failed for session ${session.ID}: ${this.describeSaveFailure(artifact)}`;
|
|
2204
2231
|
LogError(message);
|
|
2205
2232
|
return { Success: false, ErrorMessage: message, ConversationDetailLinked: false };
|
|
2206
2233
|
}
|
|
@@ -2212,7 +2239,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2212
2239
|
version.Content = contentJson;
|
|
2213
2240
|
version.UserID = contextUser.ID;
|
|
2214
2241
|
if (!(await version.Save())) {
|
|
2215
|
-
const message = `SaveSessionChannelArtifact: artifact version save failed
|
|
2242
|
+
const message = `SaveSessionChannelArtifact: artifact version save failed for session ${session.ID} (artifact ${artifact.ID}): ${this.describeSaveFailure(version)}`;
|
|
2216
2243
|
LogError(message);
|
|
2217
2244
|
return { Success: false, ErrorMessage: message, ArtifactID: artifact.ID, ConversationDetailLinked: false };
|
|
2218
2245
|
}
|
|
@@ -2270,13 +2297,22 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2270
2297
|
* queries (which scan junctions across ALL details) still surface the artifact. This is the
|
|
2271
2298
|
* least-invasive correct anchor — no fake visible message, no orphaned artifact.
|
|
2272
2299
|
*
|
|
2300
|
+
* TWO PRINCIPALS, deliberately: the lookup (`findLatestSessionDetailID`) and the junction row
|
|
2301
|
+
* itself (`saveDetailArtifactJunction`, `MJ: Conversation Detail Artifacts`) run as `runUser`
|
|
2302
|
+
* (elevated for a scoped-anonymous caller) — that junction entity is outside an anonymous
|
|
2303
|
+
* caller's relay grants. The hidden ANCHOR `Conversation Detail` this may need to create runs as
|
|
2304
|
+
* `callerUser` instead: like every other `MJ: Conversation Details` write, the System user is
|
|
2305
|
+
* refused because it is not the conversation's owner (see {@link persistDirectActionTurn},
|
|
2306
|
+
* issue #4791) — only the actual caller can create it.
|
|
2307
|
+
*
|
|
2273
2308
|
* Strictly best-effort: every failure path logs and returns; a relayed tool call NEVER fails
|
|
2274
2309
|
* because history linking did.
|
|
2275
2310
|
*/
|
|
2276
2311
|
private async linkDelegatedArtifactsToConversation(
|
|
2277
2312
|
session: MJAIAgentSessionEntity,
|
|
2278
2313
|
artifacts: DelegatedRunArtifact[] | undefined,
|
|
2279
|
-
|
|
2314
|
+
runUser: UserInfo,
|
|
2315
|
+
callerUser: UserInfo,
|
|
2280
2316
|
provider: IMetadataProvider,
|
|
2281
2317
|
): Promise<void> {
|
|
2282
2318
|
if (!artifacts || artifacts.length === 0 || !session.ConversationID) {
|
|
@@ -2284,13 +2320,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2284
2320
|
}
|
|
2285
2321
|
try {
|
|
2286
2322
|
const detailID =
|
|
2287
|
-
(await this.findLatestSessionDetailID(session,
|
|
2288
|
-
(await this.createHiddenSessionAnchorDetail(session,
|
|
2323
|
+
(await this.findLatestSessionDetailID(session, runUser, provider)) ??
|
|
2324
|
+
(await this.createHiddenSessionAnchorDetail(session, callerUser, provider));
|
|
2289
2325
|
if (!detailID) {
|
|
2290
2326
|
return; // anchor unavailable — logged in the helpers
|
|
2291
2327
|
}
|
|
2292
2328
|
for (const artifact of artifacts) {
|
|
2293
|
-
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID,
|
|
2329
|
+
await this.saveDetailArtifactJunction(detailID, artifact.ArtifactVersionID, runUser, provider);
|
|
2294
2330
|
}
|
|
2295
2331
|
} catch (error) {
|
|
2296
2332
|
LogError(`ExecuteRealtimeSessionTool: delegated-artifact history link failed: ${(error as Error).message}`);
|
|
@@ -2331,25 +2367,25 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2331
2367
|
*/
|
|
2332
2368
|
private async createHiddenSessionAnchorDetail(
|
|
2333
2369
|
session: MJAIAgentSessionEntity,
|
|
2334
|
-
|
|
2370
|
+
callerUser: UserInfo,
|
|
2335
2371
|
provider: IMetadataProvider,
|
|
2336
2372
|
): Promise<string | null> {
|
|
2337
|
-
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY,
|
|
2373
|
+
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(CONVERSATION_DETAIL_ENTITY, callerUser);
|
|
2338
2374
|
detail.NewRecord();
|
|
2339
2375
|
detail.ConversationID = session.ConversationID;
|
|
2340
2376
|
detail.Role = 'AI';
|
|
2341
2377
|
detail.HiddenToUser = true;
|
|
2342
2378
|
detail.Message = 'Artifacts produced during a realtime session (system anchor).';
|
|
2343
2379
|
detail.AgentSessionID = session.ID;
|
|
2344
|
-
// Attribute
|
|
2345
|
-
//
|
|
2380
|
+
// Attribute to the SESSION owner — the caller writing this row, whose ownership
|
|
2381
|
+
// `loadOwnedActiveSession` already proved.
|
|
2346
2382
|
detail.UserID = session.UserID;
|
|
2347
2383
|
if (await detail.Save()) {
|
|
2348
2384
|
return detail.ID;
|
|
2349
2385
|
}
|
|
2350
2386
|
LogError(
|
|
2351
2387
|
`ExecuteRealtimeSessionTool: hidden anchor detail save failed for session ${session.ID}: ` +
|
|
2352
|
-
`${detail
|
|
2388
|
+
`${this.describeSaveFailure(detail)}`,
|
|
2353
2389
|
);
|
|
2354
2390
|
return null;
|
|
2355
2391
|
}
|
|
@@ -2376,7 +2412,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2376
2412
|
if (!saved) {
|
|
2377
2413
|
LogError(
|
|
2378
2414
|
`RealtimeClientSessionResolver: artifact junction save failed for detail ${conversationDetailID} / version ${artifactVersionID}: ` +
|
|
2379
|
-
`${junction
|
|
2415
|
+
`${this.describeSaveFailure(junction)}`,
|
|
2380
2416
|
);
|
|
2381
2417
|
}
|
|
2382
2418
|
return saved;
|
|
@@ -2416,7 +2452,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2416
2452
|
if (!(await row.Save())) {
|
|
2417
2453
|
LogError(
|
|
2418
2454
|
`SaveSessionChannelArtifact: LastActiveAt stamp failed for session ${agentSessionID} / channel ${channelID}: ` +
|
|
2419
|
-
`${row
|
|
2455
|
+
`${this.describeSaveFailure(row)}`,
|
|
2420
2456
|
);
|
|
2421
2457
|
}
|
|
2422
2458
|
} catch (error) {
|
|
@@ -2560,12 +2596,6 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2560
2596
|
return minutes && minutes > 0 ? minutes * 60 : undefined;
|
|
2561
2597
|
}
|
|
2562
2598
|
|
|
2563
|
-
/**
|
|
2564
|
-
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2565
|
-
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2566
|
-
*
|
|
2567
|
-
* @returns The boolean save result (logs `CompleteMessage` on failure).
|
|
2568
|
-
*/
|
|
2569
2599
|
/**
|
|
2570
2600
|
* Stamps `RecordingStartedAt` (the recording `t0` alignment origin) + `RecordingMedia` on a
|
|
2571
2601
|
* just-started session when the browser captured WITH consent. Best-effort: a parse/save failure
|
|
@@ -2592,11 +2622,18 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2592
2622
|
session.RecordingMedia = 'Audio';
|
|
2593
2623
|
if (!(await session.Save())) {
|
|
2594
2624
|
LogError(
|
|
2595
|
-
`RealtimeClientSessionResolver.stampRecordingStart save failed
|
|
2625
|
+
`RealtimeClientSessionResolver.stampRecordingStart save failed for session ${session.ID}: ${this.describeSaveFailure(session)}`,
|
|
2596
2626
|
);
|
|
2597
2627
|
}
|
|
2598
2628
|
}
|
|
2599
2629
|
|
|
2630
|
+
/**
|
|
2631
|
+
* Persists a single transcript turn as a `Conversation Detail` stamped with the session's
|
|
2632
|
+
* conversation, the mapped role, the turn text, the session id, and the owning user.
|
|
2633
|
+
*
|
|
2634
|
+
* @returns The boolean save result. On failure it logs the session, the write user and the
|
|
2635
|
+
* reason from {@link describeSaveFailure}.
|
|
2636
|
+
*/
|
|
2600
2637
|
private async persistTranscriptTurn(
|
|
2601
2638
|
session: MJAIAgentSessionEntity,
|
|
2602
2639
|
role: string,
|
|
@@ -2626,7 +2663,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2626
2663
|
const saved = await detail.Save();
|
|
2627
2664
|
if (!saved) {
|
|
2628
2665
|
LogError(
|
|
2629
|
-
`RealtimeClientSessionResolver.persistTranscriptTurn save failed
|
|
2666
|
+
`RealtimeClientSessionResolver.persistTranscriptTurn save failed for session ${session.ID} (write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2630
2667
|
);
|
|
2631
2668
|
}
|
|
2632
2669
|
return saved;
|
|
@@ -2667,8 +2704,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2667
2704
|
// ⚠️ ELEVATING INSTEAD WAS TRIED AND IS A TRAP. `ResolveScopedAnonymousRunUser` returns
|
|
2668
2705
|
// `UserCache.GetSystemUser()`, which on this deployment resolves to the unconfigured
|
|
2669
2706
|
// placeholder `not.set@nowhere.com` ("Configured provisioning user not found; falling back
|
|
2670
|
-
// to an Owner").
|
|
2671
|
-
//
|
|
2707
|
+
// to an Owner"). That user EXISTS — it is not a missing/invalid principal — and even carries
|
|
2708
|
+
// full CRUD on `MJ: Conversation Details`. It is refused anyway:
|
|
2709
|
+
// `MJConversationDetailEntityExtended`'s owner gate accepts only the conversation's OWNER or
|
|
2710
|
+
// an Edit/Owner Resource Permission grantee, and the System user is neither. Measured on
|
|
2711
|
+
// MJ#4791 (before the core-entities fix that now records the denial): `Save()` → `false`,
|
|
2712
|
+
// `LatestResult` null — indistinguishable from a permission denial, and just as silent. The
|
|
2713
|
+
// denial itself still happens; that fix only made it stop being silent.
|
|
2672
2714
|
const writeUser = contextUser;
|
|
2673
2715
|
const rv = RunView.FromMetadataProvider(provider);
|
|
2674
2716
|
const result = await rv.RunView<MJConversationDetailEntity>(
|
|
@@ -2689,8 +2731,8 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2689
2731
|
//
|
|
2690
2732
|
// `ResultType: 'entity_object'` hands back hydrated entities, but they do not carry the
|
|
2691
2733
|
// context user the way `GetEntityObject(entity, user)` does — so `Save()` ran with no
|
|
2692
|
-
// principal and failed with an EMPTY message ("unknown error"), which is what
|
|
2693
|
-
//
|
|
2734
|
+
// principal and failed with an EMPTY message ("unknown error"), which is what an empty-message
|
|
2735
|
+
// failure here looked like before the re-load. The insert path beside this one has
|
|
2694
2736
|
// always used `GetEntityObject`; this now matches it, which is also why they now succeed
|
|
2695
2737
|
// and fail for the same reasons.
|
|
2696
2738
|
const previous = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
@@ -2722,7 +2764,7 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2722
2764
|
`RealtimeClientSessionResolver.replacePreviousTranscriptTurn save failed for session `
|
|
2723
2765
|
+ `${session.ID} — the ${mappedRole} turn keeps its PREVIOUS, shorter text and the `
|
|
2724
2766
|
+ `transcript now understates what was said: `
|
|
2725
|
-
+ `${
|
|
2767
|
+
+ `${this.describeSaveFailure(previous)}`,
|
|
2726
2768
|
);
|
|
2727
2769
|
}
|
|
2728
2770
|
return saved;
|
|
@@ -2742,6 +2784,23 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2742
2784
|
* Stored as stable facts (Role: 'AI', HiddenToUser: true, ExternalID: callId, CompletionTime: durationMs,
|
|
2743
2785
|
* Message: JSON-serialized tool facts) so that session review can reconstruct the action card dynamically
|
|
2744
2786
|
* on load (Option 1) without polluting user-facing speech chat bubbles.
|
|
2787
|
+
*
|
|
2788
|
+
* Writes as the CALLER, never the elevated run user (issue #4791). Owning the session
|
|
2789
|
+
* (`loadOwnedActiveSession`'s gate has already run on every path here) does NOT imply owning
|
|
2790
|
+
* the conversation: the conversation id was client-supplied at session start. What authorizes
|
|
2791
|
+
* this write is the save itself, evaluated for the caller — the conversation owner gate in
|
|
2792
|
+
* `MJConversationDetailEntityExtended`, plus the caller's own entity permission and Create RLS
|
|
2793
|
+
* on `MJ: Conversation Details` (for the magic-link role that RLS scopes by the invite's
|
|
2794
|
+
* `ScopeResourceID`, and the role belongs to a downstream app, not MJ core). That is why
|
|
2795
|
+
* writing as the caller is safe even though the conversation id originated client-side.
|
|
2796
|
+
* Elevating to the System user would bypass RLS entirely, and the owner gate refuses it anyway
|
|
2797
|
+
* because the System user is not the conversation owner. Measured on MJ#4791 (before the
|
|
2798
|
+
* core-entities fix that now records the denial): `Save()` returned `false` with a NULL `LatestResult` and an empty
|
|
2799
|
+
* `ResultHistory` — indistinguishable from any other silent failure. Elevating this write is
|
|
2800
|
+
* what silently dropped both hidden tool-execution turns of a real magic-link session; the
|
|
2801
|
+
* denial itself still happens today, only its silence was fixed separately.
|
|
2802
|
+
* `ResolveScopedAnonymousRunUser` elevates AI-**run**-entity writes only (prompt runs, agent
|
|
2803
|
+
* runs, the relayed dispatch itself) — never this one.
|
|
2745
2804
|
*/
|
|
2746
2805
|
private async persistDirectActionTurn(
|
|
2747
2806
|
session: MJAIAgentSessionEntity,
|
|
@@ -2757,10 +2816,9 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2757
2816
|
return false;
|
|
2758
2817
|
}
|
|
2759
2818
|
try {
|
|
2760
|
-
const writeUser = ResolveScopedAnonymousRunUser(contextUser);
|
|
2761
2819
|
const detail = await provider.GetEntityObject<MJConversationDetailEntity>(
|
|
2762
2820
|
CONVERSATION_DETAIL_ENTITY,
|
|
2763
|
-
|
|
2821
|
+
contextUser,
|
|
2764
2822
|
);
|
|
2765
2823
|
detail.NewRecord();
|
|
2766
2824
|
detail.ConversationID = session.ConversationID;
|
|
@@ -2802,13 +2860,13 @@ export class RealtimeClientSessionResolver extends ResolverBase {
|
|
|
2802
2860
|
const saved = await detail.Save();
|
|
2803
2861
|
if (!saved) {
|
|
2804
2862
|
LogError(
|
|
2805
|
-
`RealtimeClientSessionResolver.persistDirectActionTurn save failed
|
|
2863
|
+
`RealtimeClientSessionResolver.persistDirectActionTurn save failed for session ${session.ID} (tool '${toolName}', call ${callId ?? 'n/a'}, write user ${contextUser.ID}): ${this.describeSaveFailure(detail)}`,
|
|
2806
2864
|
);
|
|
2807
2865
|
}
|
|
2808
2866
|
return saved;
|
|
2809
2867
|
} catch (error) {
|
|
2810
2868
|
const message = error instanceof Error ? error.message : String(error);
|
|
2811
|
-
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error: ${message}`);
|
|
2869
|
+
LogError(`RealtimeClientSessionResolver.persistDirectActionTurn unexpected error for session ${session.ID}: ${message}`);
|
|
2812
2870
|
return false;
|
|
2813
2871
|
}
|
|
2814
2872
|
}
|
|
@@ -598,7 +598,10 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
598
598
|
detail.Message = errorMessage;
|
|
599
599
|
detail.Error = errorMessage;
|
|
600
600
|
if (!(await detail.Save())) {
|
|
601
|
-
LogError(
|
|
601
|
+
LogError(
|
|
602
|
+
`Failed to persist Error on conversation detail ${conversationDetailId}: ` +
|
|
603
|
+
`${detail.LatestResult?.CompleteMessage?.trim() || 'no failure detail recorded'}`,
|
|
604
|
+
);
|
|
602
605
|
}
|
|
603
606
|
}
|
|
604
607
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The timeout an ad-hoc query gets when the caller names none. */
|
|
2
|
+
const DEFAULT_TIMEOUT_SECONDS = 30;
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* The timeout an ad-hoc query runs with: what the caller asked for (or the default), but never
|
|
6
|
+
* longer than the server's `requestTimeout`, so a caller cannot hold a read-only connection longer
|
|
7
|
+
* than any other request may. `requestTimeoutMs` of 0 or less means the server sets no limit.
|
|
8
|
+
*/
|
|
9
|
+
export function ClampAdhocTimeoutSeconds(requested: number | undefined, requestTimeoutMs: number): number {
|
|
10
|
+
const seconds = requested != null && requested > 0 ? requested : DEFAULT_TIMEOUT_SECONDS;
|
|
11
|
+
return requestTimeoutMs > 0 ? Math.min(seconds, Math.max(1, Math.floor(requestTimeoutMs / 1000))) : seconds;
|
|
12
|
+
}
|
|
@@ -75,25 +75,26 @@ export class TaskGraphActionRunner implements TaskActionRunner {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
/**
|
|
78
|
-
* Rebuilds the action's parameters from the task's
|
|
78
|
+
* Rebuilds the action's parameters from the task's input, one parameter per key.
|
|
79
79
|
*
|
|
80
|
-
* **
|
|
81
|
-
*
|
|
82
|
-
* marked as not-logged
|
|
83
|
-
* value, which is the honest consequence of choosing not to persist it, and the
|
|
84
|
-
* dispatch is opt-in per binding rather than the default.
|
|
80
|
+
* For a **durable entity-action** graph the input was written by `RedactParamsToRecord`
|
|
81
|
+
* (`@memberjunction/actions-base`) at deferral time, so its values have been through redaction:
|
|
82
|
+
* a parameter the binding marked as not-logged arrives absent rather than secret — the action
|
|
83
|
+
* sees a missing value, which is the honest consequence of choosing not to persist it, and the
|
|
84
|
+
* reason durable dispatch is opt-in per binding rather than the default. Every other graph's
|
|
85
|
+
* input is whatever its spec authored. This method re-applies no redaction rule in either case.
|
|
85
86
|
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
87
|
+
* `InputPayload` already carries the dependency outputs: the dispatcher merges them underneath
|
|
88
|
+
* the task's own input (`mergedPayload`) before calling this runner, so a node's explicit
|
|
89
|
+
* parameters win and a step with an input mapping gets exactly the parameters it declared.
|
|
90
|
+
* `DependencyOutputs` is keyed by upstream task ID, not by parameter name, so it is deliberately
|
|
91
|
+
* not read here — spreading it would hand the action one extra parameter per dependency, named
|
|
92
|
+
* by a GUID and holding that task's whole output.
|
|
88
93
|
*/
|
|
89
94
|
private buildParams(params: TaskActionRunParams): ActionParam[] {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
merged[name] = value;
|
|
95
|
+
if (!params.InputPayload || typeof params.InputPayload !== 'object') {
|
|
96
|
+
return [];
|
|
93
97
|
}
|
|
94
|
-
|
|
95
|
-
Object.assign(merged, params.InputPayload as Record<string, unknown>);
|
|
96
|
-
}
|
|
97
|
-
return Object.entries(merged).map(([Name, Value]) => ({ Name, Value, Type: 'Input' }));
|
|
98
|
+
return Object.entries(params.InputPayload as Record<string, unknown>).map(([Name, Value]) => ({ Name, Value, Type: 'Input' }));
|
|
98
99
|
}
|
|
99
100
|
}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, dependency-free helpers for {@link AdhocQueryResolver}. Kept in their own
|
|
3
|
-
* module (no type-graphql / mssql imports) so the total-row-count boundary logic is
|
|
4
|
-
* unit-testable without standing up the GraphQL resolver or a database.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Determines the exact total row count from the page alone, when it can be known
|
|
8
|
-
* without a separate `COUNT(*)`:
|
|
9
|
-
*
|
|
10
|
-
* - **Not paging** (`maxRows == null`): the query ran uncapped, so every row came
|
|
11
|
-
* back — the total is just the number returned.
|
|
12
|
-
* - **Short page** (`pageLength < maxRows`): a page shorter than the page size means
|
|
13
|
-
* there are no rows beyond it, so the total is `startRow + pageLength` exactly.
|
|
14
|
-
* - **Full page** (`pageLength >= maxRows`): more rows may exist — returns `null` to
|
|
15
|
-
* signal that a `COUNT(*)` is required to know the true total.
|
|
16
|
-
*/
|
|
17
|
-
export declare function exactTotalFromPage(startRow: number, pageLength: number, maxRows: number | null): number | null;
|
|
18
|
-
/**
|
|
19
|
-
* Reads the total row count from a `COUNT(*)` recordset (`[{ TotalRowCount }]`),
|
|
20
|
-
* falling back to `fallback` when the count is absent, non-numeric, or negative — so
|
|
21
|
-
* an unexpected count shape never yields a misleading total. `fallback` should be a
|
|
22
|
-
* safe lower bound (e.g. `startRow + rowsReturned`).
|
|
23
|
-
*/
|
|
24
|
-
export declare function resolveAdhocTotalRowCount(countRows: ReadonlyArray<{
|
|
25
|
-
TotalRowCount?: unknown;
|
|
26
|
-
}> | null | undefined, fallback: number): number;
|
|
27
|
-
//# sourceMappingURL=adhoc-query-helpers.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adhoc-query-helpers.d.ts","sourceRoot":"","sources":["../../src/resolvers/adhoc-query-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAC9B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,MAAM,GAAG,IAAI,GACvB,MAAM,GAAG,IAAI,CAQf;AAED;;;;;GAKG;AACH,wBAAgB,yBAAyB,CACrC,SAAS,EAAE,aAAa,CAAC;IAAE,aAAa,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC,GAAG,IAAI,GAAG,SAAS,EACxE,QAAQ,EAAE,MAAM,GACjB,MAAM,CASR"}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, dependency-free helpers for {@link AdhocQueryResolver}. Kept in their own
|
|
3
|
-
* module (no type-graphql / mssql imports) so the total-row-count boundary logic is
|
|
4
|
-
* unit-testable without standing up the GraphQL resolver or a database.
|
|
5
|
-
*/
|
|
6
|
-
/**
|
|
7
|
-
* Determines the exact total row count from the page alone, when it can be known
|
|
8
|
-
* without a separate `COUNT(*)`:
|
|
9
|
-
*
|
|
10
|
-
* - **Not paging** (`maxRows == null`): the query ran uncapped, so every row came
|
|
11
|
-
* back — the total is just the number returned.
|
|
12
|
-
* - **Short page** (`pageLength < maxRows`): a page shorter than the page size means
|
|
13
|
-
* there are no rows beyond it, so the total is `startRow + pageLength` exactly.
|
|
14
|
-
* - **Full page** (`pageLength >= maxRows`): more rows may exist — returns `null` to
|
|
15
|
-
* signal that a `COUNT(*)` is required to know the true total.
|
|
16
|
-
*/
|
|
17
|
-
export function exactTotalFromPage(startRow, pageLength, maxRows) {
|
|
18
|
-
if (maxRows == null) {
|
|
19
|
-
return pageLength;
|
|
20
|
-
}
|
|
21
|
-
if (pageLength < maxRows) {
|
|
22
|
-
return startRow + pageLength;
|
|
23
|
-
}
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Reads the total row count from a `COUNT(*)` recordset (`[{ TotalRowCount }]`),
|
|
28
|
-
* falling back to `fallback` when the count is absent, non-numeric, or negative — so
|
|
29
|
-
* an unexpected count shape never yields a misleading total. `fallback` should be a
|
|
30
|
-
* safe lower bound (e.g. `startRow + rowsReturned`).
|
|
31
|
-
*/
|
|
32
|
-
export function resolveAdhocTotalRowCount(countRows, fallback) {
|
|
33
|
-
const raw = countRows?.[0]?.TotalRowCount;
|
|
34
|
-
if (raw != null) {
|
|
35
|
-
const n = Number(raw);
|
|
36
|
-
if (Number.isFinite(n) && n >= 0) {
|
|
37
|
-
return Math.floor(n);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
return fallback;
|
|
41
|
-
}
|
|
42
|
-
//# sourceMappingURL=adhoc-query-helpers.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adhoc-query-helpers.js","sourceRoot":"","sources":["../../src/resolvers/adhoc-query-helpers.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH;;;;;;;;;;GAUG;AACH,MAAM,UAAU,kBAAkB,CAC9B,QAAgB,EAChB,UAAkB,EAClB,OAAsB;IAEtB,IAAI,OAAO,IAAI,IAAI,EAAE,CAAC;QAClB,OAAO,UAAU,CAAC;IACtB,CAAC;IACD,IAAI,UAAU,GAAG,OAAO,EAAE,CAAC;QACvB,OAAO,QAAQ,GAAG,UAAU,CAAC;IACjC,CAAC;IACD,OAAO,IAAI,CAAC;AAChB,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,yBAAyB,CACrC,SAAwE,EACxE,QAAgB;IAEhB,MAAM,GAAG,GAAG,SAAS,EAAE,CAAC,CAAC,CAAC,EAAE,aAAa,CAAC;IAC1C,IAAI,GAAG,IAAI,IAAI,EAAE,CAAC;QACd,MAAM,CAAC,GAAG,MAAM,CAAC,GAAG,CAAC,CAAC;QACtB,IAAI,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/B,OAAO,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,CAAC;IACL,CAAC;IACD,OAAO,QAAQ,CAAC;AACpB,CAAC"}
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { exactTotalFromPage, resolveAdhocTotalRowCount } from '../resolvers/adhoc-query-helpers';
|
|
3
|
-
|
|
4
|
-
describe('exactTotalFromPage', () => {
|
|
5
|
-
it('returns the row count when not paging (maxRows null) — the query was uncapped', () => {
|
|
6
|
-
expect(exactTotalFromPage(0, 47, null)).toBe(47);
|
|
7
|
-
expect(exactTotalFromPage(0, 0, null)).toBe(0);
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('returns startRow + pageLength for a SHORT page (fewer rows than the page size)', () => {
|
|
11
|
-
expect(exactTotalFromPage(0, 47, 100)).toBe(47); // page 1, short → exact total
|
|
12
|
-
expect(exactTotalFromPage(200, 30, 100)).toBe(230); // page 3, short last page
|
|
13
|
-
expect(exactTotalFromPage(100, 0, 100)).toBe(100); // empty page past the end
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('returns null for a FULL page — a COUNT is required because more rows may exist', () => {
|
|
17
|
-
expect(exactTotalFromPage(0, 100, 100)).toBeNull();
|
|
18
|
-
expect(exactTotalFromPage(500, 100, 100)).toBeNull();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('treats an over-full page defensively as "needs count" (should not occur with OFFSET/FETCH)', () => {
|
|
22
|
-
expect(exactTotalFromPage(0, 150, 100)).toBeNull();
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
describe('resolveAdhocTotalRowCount', () => {
|
|
27
|
-
it('reads TotalRowCount from the count recordset', () => {
|
|
28
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: 2087 }], 100)).toBe(2087);
|
|
29
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: 0 }], 100)).toBe(0);
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
it('floors a fractional/stringified count', () => {
|
|
33
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: 42.9 }], 0)).toBe(42);
|
|
34
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: '2087' as unknown as number }], 0)).toBe(2087);
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('falls back when the count is absent, empty, null, or malformed', () => {
|
|
38
|
-
expect(resolveAdhocTotalRowCount(null, 100)).toBe(100);
|
|
39
|
-
expect(resolveAdhocTotalRowCount(undefined, 100)).toBe(100);
|
|
40
|
-
expect(resolveAdhocTotalRowCount([], 100)).toBe(100);
|
|
41
|
-
expect(resolveAdhocTotalRowCount([{}], 100)).toBe(100);
|
|
42
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: null }], 100)).toBe(100);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('falls back on non-finite or negative counts (never reports a misleading total)', () => {
|
|
46
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: Number.NaN }], 100)).toBe(100);
|
|
47
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: -5 }], 100)).toBe(100);
|
|
48
|
-
expect(resolveAdhocTotalRowCount([{ TotalRowCount: Infinity }], 100)).toBe(100);
|
|
49
|
-
});
|
|
50
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Pure, dependency-free helpers for {@link AdhocQueryResolver}. Kept in their own
|
|
3
|
-
* module (no type-graphql / mssql imports) so the total-row-count boundary logic is
|
|
4
|
-
* unit-testable without standing up the GraphQL resolver or a database.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* Determines the exact total row count from the page alone, when it can be known
|
|
9
|
-
* without a separate `COUNT(*)`:
|
|
10
|
-
*
|
|
11
|
-
* - **Not paging** (`maxRows == null`): the query ran uncapped, so every row came
|
|
12
|
-
* back — the total is just the number returned.
|
|
13
|
-
* - **Short page** (`pageLength < maxRows`): a page shorter than the page size means
|
|
14
|
-
* there are no rows beyond it, so the total is `startRow + pageLength` exactly.
|
|
15
|
-
* - **Full page** (`pageLength >= maxRows`): more rows may exist — returns `null` to
|
|
16
|
-
* signal that a `COUNT(*)` is required to know the true total.
|
|
17
|
-
*/
|
|
18
|
-
export function exactTotalFromPage(
|
|
19
|
-
startRow: number,
|
|
20
|
-
pageLength: number,
|
|
21
|
-
maxRows: number | null,
|
|
22
|
-
): number | null {
|
|
23
|
-
if (maxRows == null) {
|
|
24
|
-
return pageLength;
|
|
25
|
-
}
|
|
26
|
-
if (pageLength < maxRows) {
|
|
27
|
-
return startRow + pageLength;
|
|
28
|
-
}
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Reads the total row count from a `COUNT(*)` recordset (`[{ TotalRowCount }]`),
|
|
34
|
-
* falling back to `fallback` when the count is absent, non-numeric, or negative — so
|
|
35
|
-
* an unexpected count shape never yields a misleading total. `fallback` should be a
|
|
36
|
-
* safe lower bound (e.g. `startRow + rowsReturned`).
|
|
37
|
-
*/
|
|
38
|
-
export function resolveAdhocTotalRowCount(
|
|
39
|
-
countRows: ReadonlyArray<{ TotalRowCount?: unknown }> | null | undefined,
|
|
40
|
-
fallback: number,
|
|
41
|
-
): number {
|
|
42
|
-
const raw = countRows?.[0]?.TotalRowCount;
|
|
43
|
-
if (raw != null) {
|
|
44
|
-
const n = Number(raw);
|
|
45
|
-
if (Number.isFinite(n) && n >= 0) {
|
|
46
|
-
return Math.floor(n);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return fallback;
|
|
50
|
-
}
|