@memberjunction/server 5.37.0 → 5.39.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/README.md +79 -12
- package/dist/agents/skip-sdk.d.ts.map +1 -1
- package/dist/agents/skip-sdk.js +16 -12
- package/dist/agents/skip-sdk.js.map +1 -1
- package/dist/apolloServer/index.d.ts +0 -8
- package/dist/apolloServer/index.d.ts.map +1 -1
- package/dist/apolloServer/index.js +61 -0
- package/dist/apolloServer/index.js.map +1 -1
- package/dist/config.d.ts +68 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +24 -0
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +2 -21
- package/dist/context.js.map +1 -1
- package/dist/generated/generated.d.ts +172 -5
- package/dist/generated/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +867 -14
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +51 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -0
- package/dist/generic/FireAndForgetHeartbeat.js +44 -0
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +177 -16
- package/dist/index.js.map +1 -1
- package/dist/logging/NoLog.d.ts +50 -0
- package/dist/logging/NoLog.d.ts.map +1 -0
- package/dist/logging/NoLog.js +80 -0
- package/dist/logging/NoLog.js.map +1 -0
- package/dist/logging/bootAudit.d.ts +43 -0
- package/dist/logging/bootAudit.d.ts.map +1 -0
- package/dist/logging/bootAudit.js +83 -0
- package/dist/logging/bootAudit.js.map +1 -0
- package/dist/logging/boundaryLogPayload.d.ts +18 -0
- package/dist/logging/boundaryLogPayload.d.ts.map +1 -0
- package/dist/logging/boundaryLogPayload.js +18 -0
- package/dist/logging/boundaryLogPayload.js.map +1 -0
- package/dist/logging/secretRedactor.d.ts +23 -0
- package/dist/logging/secretRedactor.d.ts.map +1 -0
- package/dist/logging/secretRedactor.js +53 -0
- package/dist/logging/secretRedactor.js.map +1 -0
- package/dist/logging/shortenForLog.d.ts +8 -0
- package/dist/logging/shortenForLog.d.ts.map +1 -0
- package/dist/logging/shortenForLog.js +21 -0
- package/dist/logging/shortenForLog.js.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts +22 -0
- package/dist/logging/variablesLoggingMiddleware.d.ts.map +1 -0
- package/dist/logging/variablesLoggingMiddleware.js +127 -0
- package/dist/logging/variablesLoggingMiddleware.js.map +1 -0
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +8 -4
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts +259 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +1276 -117
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/IntegrationProgressResolver.d.ts +90 -0
- package/dist/resolvers/IntegrationProgressResolver.d.ts.map +1 -0
- package/dist/resolvers/IntegrationProgressResolver.js +196 -0
- package/dist/resolvers/IntegrationProgressResolver.js.map +1 -0
- package/dist/resolvers/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +19 -14
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +5 -4
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +6 -18
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +32 -64
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +12 -2
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SearchEntitiesResolver.d.ts +46 -0
- package/dist/resolvers/SearchEntitiesResolver.d.ts.map +1 -0
- package/dist/resolvers/SearchEntitiesResolver.js +216 -0
- package/dist/resolvers/SearchEntitiesResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -1
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/services/ScheduledJobsService.d.ts.map +1 -1
- package/dist/services/ScheduledJobsService.js +14 -2
- package/dist/services/ScheduledJobsService.js.map +1 -1
- package/package.json +75 -74
- package/src/__tests__/NoLog.test.ts +76 -0
- package/src/__tests__/bootAudit.test.ts +188 -0
- package/src/__tests__/boundaryLogPayload.test.ts +31 -0
- package/src/__tests__/getDataTokenRedaction.test.ts +84 -0
- package/src/__tests__/resolverBase.rls.test.ts +224 -0
- package/src/__tests__/secretRedactor.test.ts +163 -0
- package/src/__tests__/subscriptionRedaction.test.ts +217 -0
- package/src/agents/skip-sdk.ts +16 -13
- package/src/apolloServer/index.ts +58 -0
- package/src/config.ts +27 -0
- package/src/context.ts +2 -19
- package/src/generated/generated.ts +627 -15
- package/src/generic/FireAndForgetHeartbeat.ts +85 -0
- package/src/generic/__tests__/FireAndForgetHeartbeat.test.ts +99 -0
- package/src/index.ts +199 -19
- package/src/logging/NoLog.ts +88 -0
- package/src/logging/bootAudit.ts +117 -0
- package/src/logging/boundaryLogPayload.ts +19 -0
- package/src/logging/secretRedactor.ts +82 -0
- package/src/logging/shortenForLog.ts +17 -0
- package/src/logging/variablesLoggingMiddleware.ts +191 -0
- package/src/resolvers/GetDataResolver.ts +9 -5
- package/src/resolvers/IntegrationDiscoveryResolver.ts +1111 -120
- package/src/resolvers/IntegrationProgressResolver.ts +220 -0
- package/src/resolvers/MCPResolver.ts +18 -14
- package/src/resolvers/QueryResolver.ts +7 -6
- package/src/resolvers/QuerySystemUserResolver.ts +11 -25
- package/src/resolvers/RunAIAgentResolver.ts +34 -71
- package/src/resolvers/RunTestResolver.ts +14 -2
- package/src/resolvers/SearchEntitiesResolver.ts +173 -0
- package/src/resolvers/VectorizeEntityResolver.ts +14 -1
- package/src/resolvers/__tests__/IntegrationProgressResolver.test.ts +170 -0
- package/src/services/ScheduledJobsService.ts +15 -2
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IntegrationProgressResolver — GraphQL subscription surface for long-running
|
|
3
|
+
* integration events (plan.md §11).
|
|
4
|
+
*
|
|
5
|
+
* Exposes a SINGLE @Subscription topic carrying structured progress notifications
|
|
6
|
+
* for every kind of integration run — metadata refresh, connector creation, sync,
|
|
7
|
+
* and RSU — so a UI can stream live updates instead of polling
|
|
8
|
+
* IntegrationTailRunEvents. Filtering by `runID`, `companyIntegrationID`, and `kind`
|
|
9
|
+
* happens in the subscription filter, exactly like PipelineProgressResolver.
|
|
10
|
+
*
|
|
11
|
+
* Wiring through the MJ event system: server-side producers (the
|
|
12
|
+
* IntegrationProgressEmitter, the connector-creation pipeline, the sync engine,
|
|
13
|
+
* the RSU pipeline) call the exported {@link PublishIntegrationProgress} helper,
|
|
14
|
+
* which forwards onto the same `PubSubManager` PubSub infrastructure that backs
|
|
15
|
+
* PipelineProgress and cache-invalidation events — NOT a parallel mechanism.
|
|
16
|
+
*/
|
|
17
|
+
import { Resolver, Subscription, Root, ObjectType, Field, Int, Arg } from 'type-graphql';
|
|
18
|
+
import { LogError } from '@memberjunction/core';
|
|
19
|
+
import type {
|
|
20
|
+
IntegrationRunKind,
|
|
21
|
+
IntegrationProgressEventType,
|
|
22
|
+
IntegrationProgressLevel,
|
|
23
|
+
} from '@memberjunction/integration-progress-artifacts';
|
|
24
|
+
import { PubSubManager } from '../generic/PubSubManager.js';
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* The single PubSub topic all integration progress notifications ride. Mirrors
|
|
28
|
+
* PIPELINE_PROGRESS_TOPIC — one topic, filtered per-subscriber by run/connector/kind.
|
|
29
|
+
*/
|
|
30
|
+
export const INTEGRATION_PROGRESS_TOPIC = 'INTEGRATION_PROGRESS';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Logical channels (the "topic enum" of plan.md §11) a subscriber can scope to.
|
|
34
|
+
* These map 1:1 onto a subset of {@link IntegrationRunKind} so a UI can subscribe
|
|
35
|
+
* to, e.g., only sync progress without parsing every notification. Expressed as a
|
|
36
|
+
* string union (per the union-types-over-enums convention) and surfaced over
|
|
37
|
+
* GraphQL as a plain String argument/field.
|
|
38
|
+
*/
|
|
39
|
+
export type IntegrationProgressTopic =
|
|
40
|
+
| 'MetadataRefresh'
|
|
41
|
+
| 'ConnectorCreation'
|
|
42
|
+
| 'Sync'
|
|
43
|
+
| 'RSU';
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Maps an {@link IntegrationRunKind} (the vocabulary the emitter writes) onto the
|
|
47
|
+
* coarser {@link IntegrationProgressTopic} channel a subscriber filters by. Kinds
|
|
48
|
+
* without a dedicated channel resolve to `undefined` and only match unscoped
|
|
49
|
+
* subscriptions.
|
|
50
|
+
*/
|
|
51
|
+
const RUN_KIND_TO_TOPIC: Readonly<Record<IntegrationRunKind, IntegrationProgressTopic | undefined>> = {
|
|
52
|
+
Discovery: 'MetadataRefresh',
|
|
53
|
+
Enrichment: 'MetadataRefresh',
|
|
54
|
+
ConnectorCreation: 'ConnectorCreation',
|
|
55
|
+
SyncRun: 'Sync',
|
|
56
|
+
RSU: 'RSU',
|
|
57
|
+
TableCreation: 'RSU',
|
|
58
|
+
Other: undefined,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
/** Resolve the logical channel for a run kind (helper kept tiny + testable). */
|
|
62
|
+
export function IntegrationProgressTopicForKind(kind: IntegrationRunKind): IntegrationProgressTopic | undefined {
|
|
63
|
+
return RUN_KIND_TO_TOPIC[kind];
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A single integration progress notification pushed to the subscription topic.
|
|
68
|
+
* Carries the structured shape plan.md §11 asks for — the subsystem-specific
|
|
69
|
+
* `data` payload is serialized into `DataJSON` so the GraphQL field stays a scalar.
|
|
70
|
+
*/
|
|
71
|
+
@ObjectType()
|
|
72
|
+
export class IntegrationProgressNotification {
|
|
73
|
+
/** The run this event belongs to (matches manifest.runID). */
|
|
74
|
+
@Field(() => String)
|
|
75
|
+
RunID: string;
|
|
76
|
+
|
|
77
|
+
/** The connector (CompanyIntegration) the run is for, when known. */
|
|
78
|
+
@Field(() => String, { nullable: true })
|
|
79
|
+
CompanyIntegrationID?: string;
|
|
80
|
+
|
|
81
|
+
/** What kind of run produced this event (IntegrationRunKind, as a string). */
|
|
82
|
+
@Field(() => String)
|
|
83
|
+
Kind: string;
|
|
84
|
+
|
|
85
|
+
/** The logical channel this notification belongs to (IntegrationProgressTopic). */
|
|
86
|
+
@Field(() => String, { nullable: true })
|
|
87
|
+
Topic?: string;
|
|
88
|
+
|
|
89
|
+
/** The progress event type (IntegrationProgressEventType, as a string). */
|
|
90
|
+
@Field(() => String)
|
|
91
|
+
EventType: string;
|
|
92
|
+
|
|
93
|
+
/** Monotonic sequence number within the run. */
|
|
94
|
+
@Field(() => Int)
|
|
95
|
+
Seq: number;
|
|
96
|
+
|
|
97
|
+
/** Free-form human message (optional). */
|
|
98
|
+
@Field(() => String, { nullable: true })
|
|
99
|
+
Message?: string;
|
|
100
|
+
|
|
101
|
+
/** Stage name within the run (optional). */
|
|
102
|
+
@Field(() => String, { nullable: true })
|
|
103
|
+
Stage?: string;
|
|
104
|
+
|
|
105
|
+
/** Severity level (info/warn/error/debug). */
|
|
106
|
+
@Field(() => String, { nullable: true })
|
|
107
|
+
Level?: string;
|
|
108
|
+
|
|
109
|
+
/** Subsystem-specific structured payload, JSON-serialized. */
|
|
110
|
+
@Field(() => String, { nullable: true })
|
|
111
|
+
DataJSON?: string;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Arguments the emitter/pipeline pass to {@link PublishIntegrationProgress}. The
|
|
116
|
+
* helper derives `Topic` and serializes `Data`, so callers pass the raw event shape.
|
|
117
|
+
*/
|
|
118
|
+
export interface PublishIntegrationProgressArgs {
|
|
119
|
+
RunID: string;
|
|
120
|
+
Kind: IntegrationRunKind;
|
|
121
|
+
EventType: IntegrationProgressEventType;
|
|
122
|
+
Seq: number;
|
|
123
|
+
CompanyIntegrationID?: string;
|
|
124
|
+
Message?: string;
|
|
125
|
+
Stage?: string;
|
|
126
|
+
Level?: IntegrationProgressLevel;
|
|
127
|
+
Data?: Record<string, unknown>;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/** Predicate used by the @Subscription filter — exported so it is unit-testable. */
|
|
131
|
+
export function MatchesIntegrationSubscription(
|
|
132
|
+
payload: IntegrationProgressNotification,
|
|
133
|
+
args: { runID?: string; companyIntegrationID?: string; topic?: string }
|
|
134
|
+
): boolean {
|
|
135
|
+
if (args.runID && payload.RunID !== args.runID) return false;
|
|
136
|
+
if (args.companyIntegrationID && payload.CompanyIntegrationID !== args.companyIntegrationID) return false;
|
|
137
|
+
if (args.topic && payload.Topic !== args.topic) return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Build the notification payload from publish args. Pure + side-effect free so the
|
|
143
|
+
* shape (including JSON serialization + topic derivation) can be asserted in tests
|
|
144
|
+
* without touching PubSub.
|
|
145
|
+
*/
|
|
146
|
+
export function BuildIntegrationProgressNotification(
|
|
147
|
+
args: PublishIntegrationProgressArgs
|
|
148
|
+
): IntegrationProgressNotification {
|
|
149
|
+
return {
|
|
150
|
+
RunID: args.RunID,
|
|
151
|
+
CompanyIntegrationID: args.CompanyIntegrationID,
|
|
152
|
+
Kind: args.Kind,
|
|
153
|
+
Topic: IntegrationProgressTopicForKind(args.Kind),
|
|
154
|
+
EventType: args.EventType,
|
|
155
|
+
Seq: args.Seq,
|
|
156
|
+
Message: args.Message,
|
|
157
|
+
Stage: args.Stage,
|
|
158
|
+
Level: args.Level,
|
|
159
|
+
DataJSON: serializeData(args.Data),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/** JSON-serialize the subsystem payload, swallowing cyclic/serialization failures. */
|
|
164
|
+
function serializeData(data?: Record<string, unknown>): string | undefined {
|
|
165
|
+
if (data == null) return undefined;
|
|
166
|
+
try {
|
|
167
|
+
return JSON.stringify(data);
|
|
168
|
+
} catch (e) {
|
|
169
|
+
LogError(`IntegrationProgressResolver: failed to serialize data payload: ${e}`);
|
|
170
|
+
return undefined;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Publish an integration progress event onto the subscription topic. This is the
|
|
176
|
+
* entry point server-side producers (IntegrationProgressEmitter, pipelines, sync
|
|
177
|
+
* engine, RSU) call to push live updates to subscribed clients. Uses the shared
|
|
178
|
+
* PubSubManager — the same infrastructure as PipelineProgress and cache events.
|
|
179
|
+
*
|
|
180
|
+
* Returns the published notification (handy for tests / logging); returns
|
|
181
|
+
* undefined and logs if publishing fails (never throws into the producer path).
|
|
182
|
+
*/
|
|
183
|
+
export function PublishIntegrationProgress(
|
|
184
|
+
args: PublishIntegrationProgressArgs
|
|
185
|
+
): IntegrationProgressNotification | undefined {
|
|
186
|
+
try {
|
|
187
|
+
const notification = BuildIntegrationProgressNotification(args);
|
|
188
|
+
PubSubManager.Instance.Publish(INTEGRATION_PROGRESS_TOPIC, { ...notification });
|
|
189
|
+
return notification;
|
|
190
|
+
} catch (e) {
|
|
191
|
+
LogError(`PublishIntegrationProgress failed: ${e}`);
|
|
192
|
+
return undefined;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
@Resolver()
|
|
197
|
+
export class IntegrationProgressResolver {
|
|
198
|
+
/**
|
|
199
|
+
* Subscribe to integration progress notifications. With no arguments the
|
|
200
|
+
* subscriber receives every integration event on this server; pass any of
|
|
201
|
+
* `runID`, `companyIntegrationID`, or `topic` to scope the stream.
|
|
202
|
+
*/
|
|
203
|
+
@Subscription(() => IntegrationProgressNotification, {
|
|
204
|
+
topics: INTEGRATION_PROGRESS_TOPIC,
|
|
205
|
+
filter: ({ payload, args }: {
|
|
206
|
+
payload: IntegrationProgressNotification;
|
|
207
|
+
args: { runID?: string; companyIntegrationID?: string; topic?: string };
|
|
208
|
+
}) => MatchesIntegrationSubscription(payload, args),
|
|
209
|
+
})
|
|
210
|
+
IntegrationProgress(
|
|
211
|
+
@Root() notification: IntegrationProgressNotification,
|
|
212
|
+
@Arg('runID', () => String, { nullable: true }) _runID?: string,
|
|
213
|
+
@Arg('companyIntegrationID', () => String, { nullable: true }) _companyIntegrationID?: string,
|
|
214
|
+
@Arg('topic', () => String, { nullable: true }) _topic?: string
|
|
215
|
+
): IntegrationProgressNotification {
|
|
216
|
+
return notification;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export default IntegrationProgressResolver;
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import { Resolver, Mutation, Query, Subscription, Arg, Ctx, Root, Field, Int, ObjectType, InputType, PubSub, registerEnumType } from 'type-graphql';
|
|
9
9
|
import { PubSubEngine } from 'type-graphql';
|
|
10
|
-
import { LogError, LogStatus, UserInfo, Metadata, RunView } from '@memberjunction/core';
|
|
10
|
+
import { LogError, LogStatus, LogStatusEx, UserInfo, Metadata, RunView } from '@memberjunction/core';
|
|
11
11
|
import {
|
|
12
12
|
MCPClientManager,
|
|
13
13
|
MCPSyncToolsResult,
|
|
@@ -705,25 +705,25 @@ export class MCPResolver extends ResolverBase {
|
|
|
705
705
|
|
|
706
706
|
try {
|
|
707
707
|
// Check API key scope authorization
|
|
708
|
-
|
|
708
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 1 - Checking API key authorization...`, verboseOnly: true });
|
|
709
709
|
await this.CheckAPIKeyScopeAuthorization('mcp:execute', ConnectionID, ctx.userPayload);
|
|
710
|
-
|
|
710
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 1 complete - Authorization passed (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
711
711
|
|
|
712
712
|
// Get the MCP client manager instance and ensure it's initialized
|
|
713
|
-
|
|
713
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 2 - Initializing MCP client manager...`, verboseOnly: true });
|
|
714
714
|
const manager = MCPClientManager.Instance;
|
|
715
715
|
const publicUrl = this.getPublicUrl();
|
|
716
716
|
await manager.initialize(user, { publicUrl });
|
|
717
|
-
|
|
717
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 2 complete - Manager initialized (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
718
718
|
|
|
719
719
|
// Connect if not already connected
|
|
720
720
|
const isConnected = manager.isConnected(ConnectionID);
|
|
721
|
-
|
|
721
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 3 - Connection status: ${isConnected ? 'already connected' : 'needs connection'}`, verboseOnly: true });
|
|
722
722
|
if (!isConnected) {
|
|
723
723
|
LogStatus(`MCPResolver: [${ToolName}] Connecting to MCP server for connection ${ConnectionID}...`);
|
|
724
724
|
try {
|
|
725
725
|
await manager.connect(ConnectionID, { contextUser: user });
|
|
726
|
-
|
|
726
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 3 complete - Connected (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
727
727
|
} catch (connectError) {
|
|
728
728
|
// Check for OAuth authorization required
|
|
729
729
|
if (connectError instanceof OAuthAuthorizationRequiredError) {
|
|
@@ -763,12 +763,16 @@ export class MCPResolver extends ResolverBase {
|
|
|
763
763
|
}
|
|
764
764
|
|
|
765
765
|
// Parse input arguments
|
|
766
|
-
|
|
766
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 4 - Parsing input arguments...`, verboseOnly: true });
|
|
767
767
|
let parsedArgs: Record<string, unknown> = {};
|
|
768
768
|
if (InputArgs) {
|
|
769
769
|
try {
|
|
770
770
|
parsedArgs = JSON.parse(InputArgs);
|
|
771
|
-
|
|
771
|
+
if (configInfo.loggingSettings.graphql.logVariables) {
|
|
772
|
+
LogStatus(`MCPResolver: [${ToolName}] Parsed args: ${JSON.stringify(parsedArgs)}`);
|
|
773
|
+
} else {
|
|
774
|
+
LogStatus(`MCPResolver: [${ToolName}] tool invoked`);
|
|
775
|
+
}
|
|
772
776
|
} catch (parseError) {
|
|
773
777
|
LogError(`MCPResolver: [${ToolName}] Failed to parse InputArgs: ${parseError}`);
|
|
774
778
|
return {
|
|
@@ -777,18 +781,18 @@ export class MCPResolver extends ResolverBase {
|
|
|
777
781
|
};
|
|
778
782
|
}
|
|
779
783
|
}
|
|
780
|
-
|
|
784
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 4 complete - Args parsed (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
781
785
|
|
|
782
786
|
// Call the tool
|
|
783
|
-
|
|
784
|
-
|
|
787
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 5 - Calling tool on connection ${ConnectionID}...`, verboseOnly: true });
|
|
788
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Tool ID: ${ToolID}`, verboseOnly: true });
|
|
785
789
|
const result: MCPToolCallResult = await manager.callTool(
|
|
786
790
|
ConnectionID,
|
|
787
791
|
ToolName,
|
|
788
792
|
{ arguments: parsedArgs },
|
|
789
793
|
{ contextUser: user }
|
|
790
794
|
);
|
|
791
|
-
|
|
795
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 5 complete - Tool call returned (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
792
796
|
|
|
793
797
|
// Format the result for the response - wrap in object for GraphQLJSONObject
|
|
794
798
|
let formattedResult: Record<string, unknown> | null = null;
|
|
@@ -827,7 +831,7 @@ export class MCPResolver extends ResolverBase {
|
|
|
827
831
|
formattedResult = result.structuredContent as Record<string, unknown>;
|
|
828
832
|
}
|
|
829
833
|
|
|
830
|
-
|
|
834
|
+
LogStatusEx({ message: `MCPResolver: [${ToolName}] Step 6 complete - Result formatted (${Date.now() - startTime}ms)`, verboseOnly: true });
|
|
831
835
|
LogStatus(`MCPResolver: [${ToolName}] Tool execution complete - Success: ${result.success}, Duration: ${result.durationMs}ms, Total time: ${Date.now() - startTime}ms`);
|
|
832
836
|
|
|
833
837
|
return {
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { Arg, Ctx, ObjectType, Query, Resolver, Field, Int, InputType } from 'type-graphql';
|
|
2
|
-
import { RunQuery,
|
|
2
|
+
import { RunQuery, IRunQueryProvider, IMetadataProvider, RunQueryParams, LogError, RunQueryWithCacheCheckParams, RunQueriesWithCacheCheckResponse, RunQueryWithCacheCheckResult } from '@memberjunction/core';
|
|
3
3
|
import { AppContext } from '../types.js';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
5
|
import { GraphQLJSONObject } from 'graphql-type-json';
|
|
6
6
|
import { Metadata } from '@memberjunction/core';
|
|
7
|
+
import { MJQueryEntity, QueryEngine } from '@memberjunction/core-entities';
|
|
7
8
|
import { GetReadOnlyProvider } from '../util.js';
|
|
8
9
|
import { SQLServerDataProvider } from '@memberjunction/sqlserver-dataprovider';
|
|
9
10
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
@@ -149,9 +150,9 @@ export class RunQueriesWithCacheCheckOutput {
|
|
|
149
150
|
|
|
150
151
|
@Resolver()
|
|
151
152
|
export class RunQueryResolver extends ResolverBase {
|
|
152
|
-
private async findQuery(md: IMetadataProvider, QueryID: string, QueryName?: string, CategoryID?: string, CategoryPath?: string, refreshMetadataIfNotFound: boolean = false): Promise<
|
|
153
|
-
|
|
154
|
-
const queries =
|
|
153
|
+
private async findQuery(md: IMetadataProvider, QueryID: string, QueryName?: string, CategoryID?: string, CategoryPath?: string, refreshMetadataIfNotFound: boolean = false): Promise<MJQueryEntity | null> {
|
|
154
|
+
const qe = QueryEngine.Instance;
|
|
155
|
+
const queries = qe.Queries.filter(q => {
|
|
155
156
|
if (QueryID) {
|
|
156
157
|
return q.ID.trim().toLowerCase() === QueryID.trim().toLowerCase();
|
|
157
158
|
} else if (QueryName) {
|
|
@@ -169,8 +170,8 @@ export class RunQueryResolver extends ResolverBase {
|
|
|
169
170
|
|
|
170
171
|
if (queries.length === 0) {
|
|
171
172
|
if (refreshMetadataIfNotFound) {
|
|
172
|
-
// If we didn't find the query, refresh
|
|
173
|
-
await
|
|
173
|
+
// If we didn't find the query, force-refresh QueryEngine and retry
|
|
174
|
+
await QueryEngine.Instance.Config(true);
|
|
174
175
|
return this.findQuery(md, QueryID, QueryName, CategoryID, CategoryPath, false); // change the refresh flag to false so we don't loop infinitely
|
|
175
176
|
}
|
|
176
177
|
else {
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Arg, Ctx, Field, InputType, Mutation, ObjectType, registerEnumType, Resolver, PubSub, PubSubEngine } from 'type-graphql';
|
|
2
2
|
import { AppContext } from '../types.js';
|
|
3
|
-
import { LogError,
|
|
3
|
+
import { LogError, UserInfo, CompositeKey, DatabaseProviderBase, LogStatus } from '@memberjunction/core';
|
|
4
4
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
5
|
-
import { MJQueryCategoryEntity, MJQueryPermissionEntity } from '@memberjunction/core-entities';
|
|
5
|
+
import { MJQueryCategoryEntity, MJQueryPermissionEntity, MJQueryFieldEntity, MJQueryParameterEntity, MJQueryEntityEntity, QueryEngine } from '@memberjunction/core-entities';
|
|
6
6
|
import { MJQueryResolver, MJQuery_, MJQueryField_, MJQueryParameter_, MJQueryEntity_, MJQueryPermission_ } from '../generated/generated.js';
|
|
7
7
|
import { GetReadWriteProvider } from '../util.js';
|
|
8
8
|
import { DeleteOptionsInput } from '../generic/DeleteOptionsInput.js';
|
|
@@ -301,12 +301,9 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
301
301
|
|
|
302
302
|
if (input.Permissions && input.Permissions.length > 0) {
|
|
303
303
|
await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
304
|
-
|
|
304
|
+
|
|
305
305
|
}
|
|
306
306
|
|
|
307
|
-
// Refresh metadata cache to include the newly created query
|
|
308
|
-
// This ensures subsequent operations can find the query without additional DB calls
|
|
309
|
-
await provider.Refresh();
|
|
310
307
|
|
|
311
308
|
return this.buildSuccessResult(record);
|
|
312
309
|
}
|
|
@@ -365,7 +362,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
365
362
|
};
|
|
366
363
|
}
|
|
367
364
|
|
|
368
|
-
private mapFields(fields:
|
|
365
|
+
private mapFields(fields: MJQueryFieldEntity[]): MJQueryField_[] {
|
|
369
366
|
return fields.map(f => ({
|
|
370
367
|
ID: f.ID,
|
|
371
368
|
QueryID: f.QueryID,
|
|
@@ -389,7 +386,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
389
386
|
}) as MJQueryField_);
|
|
390
387
|
}
|
|
391
388
|
|
|
392
|
-
private mapParameters(params:
|
|
389
|
+
private mapParameters(params: MJQueryParameterEntity[]): MJQueryParameter_[] {
|
|
393
390
|
return params.map(p => ({
|
|
394
391
|
ID: p.ID,
|
|
395
392
|
QueryID: p.QueryID,
|
|
@@ -408,7 +405,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
408
405
|
}) as MJQueryParameter_);
|
|
409
406
|
}
|
|
410
407
|
|
|
411
|
-
private mapEntities(entities:
|
|
408
|
+
private mapEntities(entities: MJQueryEntityEntity[]): MJQueryEntity_[] {
|
|
412
409
|
return entities.map(e => ({
|
|
413
410
|
ID: e.ID,
|
|
414
411
|
QueryID: e.QueryID,
|
|
@@ -422,7 +419,7 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
422
419
|
}) as MJQueryEntity_);
|
|
423
420
|
}
|
|
424
421
|
|
|
425
|
-
private mapPermissions(permissions:
|
|
422
|
+
private mapPermissions(permissions: MJQueryPermissionEntity[]): MJQueryPermission_[] {
|
|
426
423
|
return permissions.map(p => ({
|
|
427
424
|
ID: p.ID,
|
|
428
425
|
QueryID: p.QueryID,
|
|
@@ -528,25 +525,14 @@ export class MJQueryResolverExtended extends MJQueryResolver {
|
|
|
528
525
|
|
|
529
526
|
// Handle permissions update if provided
|
|
530
527
|
if (input.Permissions !== undefined) {
|
|
531
|
-
// Delete existing permissions
|
|
532
|
-
const
|
|
533
|
-
const
|
|
534
|
-
|
|
535
|
-
ExtraFilter: `QueryID='${queryID}'`,
|
|
536
|
-
ResultType: 'entity_object'
|
|
537
|
-
}, context.userPayload.userRecord);
|
|
538
|
-
|
|
539
|
-
if (existingPermissions.Success && existingPermissions.Results) {
|
|
540
|
-
for (const perm of existingPermissions.Results) {
|
|
541
|
-
await perm.Delete();
|
|
542
|
-
}
|
|
528
|
+
// Delete existing permissions (read from QueryEngine cache)
|
|
529
|
+
const existingPermissions = QueryEngine.Instance.GetQueryPermissions(queryID);
|
|
530
|
+
for (const perm of existingPermissions) {
|
|
531
|
+
await perm.Delete();
|
|
543
532
|
}
|
|
544
533
|
|
|
545
534
|
// Create new permissions
|
|
546
535
|
await this.createPermissions(provider, input.Permissions, queryID, context.userPayload.userRecord);
|
|
547
|
-
|
|
548
|
-
// Refresh the metadata to get updated permissions
|
|
549
|
-
await queryEntity.RefreshRelatedMetadata(true);
|
|
550
536
|
}
|
|
551
537
|
|
|
552
538
|
return this.buildSuccessResult(queryEntity);
|
|
@@ -9,6 +9,7 @@ import { AIEngine } from '@memberjunction/aiengine';
|
|
|
9
9
|
import { ChatMessage, ChatMessageContent } from '@memberjunction/ai';
|
|
10
10
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
11
11
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
12
|
+
import { startLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
|
|
12
13
|
import { RequireSystemUser } from '../directives/RequireSystemUser.js';
|
|
13
14
|
import { GetReadWriteProvider } from '../util.js';
|
|
14
15
|
import { SafeJSONParse, UUIDsEqual } from '@memberjunction/global';
|
|
@@ -242,6 +243,13 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
242
243
|
*/
|
|
243
244
|
private createProgressCallback(pubSub: PubSubEngine, sessionId: string, userPayload: UserPayload, agentRunRef: { current: any }) {
|
|
244
245
|
return (progress: any) => {
|
|
246
|
+
// Capture the agent run into the ref as soon as any progress event carries it (even
|
|
247
|
+
// "noise" steps), so the fire-and-forget liveness pulse can read its id/status mid-run
|
|
248
|
+
// rather than only after RunAgentInConversation returns.
|
|
249
|
+
if (progress.metadata?.agentRun) {
|
|
250
|
+
agentRunRef.current = progress.metadata.agentRun;
|
|
251
|
+
}
|
|
252
|
+
|
|
245
253
|
// Only publish progress for significant steps (not initialization noise)
|
|
246
254
|
const significantSteps = ['prompt_execution', 'action_execution', 'subagent_execution', 'decision_processing'];
|
|
247
255
|
if (!significantSteps.includes(progress.step)) {
|
|
@@ -373,7 +381,10 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
373
381
|
sourceArtifactId?: string,
|
|
374
382
|
sourceArtifactVersionId?: string,
|
|
375
383
|
/** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
|
|
376
|
-
conversationId?: string
|
|
384
|
+
conversationId?: string,
|
|
385
|
+
/** Optional external ref the caller can read to observe the agent run as it becomes available
|
|
386
|
+
* (used by the fire-and-forget liveness pulse to enrich heartbeats with the run id/status). */
|
|
387
|
+
runRef?: { current: MJAIAgentRunEntityExtended | null }
|
|
377
388
|
): Promise<AIAgentRunResult> {
|
|
378
389
|
const startTime = Date.now();
|
|
379
390
|
|
|
@@ -407,8 +418,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
407
418
|
// singleton's transaction state with concurrent requests (e.g. conversation deletes).
|
|
408
419
|
const agentRunner = new AgentRunner(p);
|
|
409
420
|
|
|
410
|
-
// Track agent run for streaming (use ref to update later)
|
|
411
|
-
|
|
421
|
+
// Track agent run for streaming (use ref to update later). Reuse the caller-supplied
|
|
422
|
+
// ref when provided so the fire-and-forget liveness pulse can observe the run.
|
|
423
|
+
const agentRunRef = runRef ?? { current: null as any };
|
|
412
424
|
|
|
413
425
|
console.log(`🚀 Starting agent execution with sessionId: ${sessionId}`);
|
|
414
426
|
|
|
@@ -1225,12 +1237,23 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1225
1237
|
/** LATENCY OPT #2: Pre-resolved conversationId avoids redundant DB load in AgentRunner */
|
|
1226
1238
|
conversationId?: string
|
|
1227
1239
|
): void {
|
|
1240
|
+
// Ref the liveness pulse reads to enrich heartbeats once the run is created.
|
|
1241
|
+
const runRef: { current: MJAIAgentRunEntityExtended | null } = { current: null };
|
|
1242
|
+
const pulse = startLivenessPulse({
|
|
1243
|
+
pubSub,
|
|
1244
|
+
sessionId,
|
|
1245
|
+
resolver: 'RunAIAgentResolver',
|
|
1246
|
+
readStatus: () => runRef.current
|
|
1247
|
+
? { runId: runRef.current.ID, status: runRef.current.Status }
|
|
1248
|
+
: undefined,
|
|
1249
|
+
});
|
|
1250
|
+
|
|
1228
1251
|
// Execute in background - errors are handled within, not propagated
|
|
1229
1252
|
this.executeAIAgent(
|
|
1230
1253
|
p, dataSource, agentId, userPayload, messagesJson, sessionId, pubSub,
|
|
1231
1254
|
data, payload, undefined, lastRunId, autoPopulateLastRunPayload,
|
|
1232
1255
|
configurationId, conversationDetailId, createArtifacts, createNotification,
|
|
1233
|
-
sourceArtifactId, sourceArtifactVersionId, conversationId
|
|
1256
|
+
sourceArtifactId, sourceArtifactVersionId, conversationId, runRef
|
|
1234
1257
|
).catch((error: unknown) => {
|
|
1235
1258
|
// Background execution failed unexpectedly (executeAIAgent has its own try-catch,
|
|
1236
1259
|
// so this would only fire for truly unexpected errors).
|
|
@@ -1249,7 +1272,7 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1249
1272
|
result: JSON.stringify({ success: false, errorMessage })
|
|
1250
1273
|
};
|
|
1251
1274
|
this.PublishStreamingUpdate(pubSub, errorCompletionData, userPayload);
|
|
1252
|
-
});
|
|
1275
|
+
}).finally(() => pulse.stop());
|
|
1253
1276
|
}
|
|
1254
1277
|
|
|
1255
1278
|
/**
|
|
@@ -1298,13 +1321,13 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1298
1321
|
// Reverse to get chronological order (oldest first)
|
|
1299
1322
|
const details = detailsResult.Results.reverse();
|
|
1300
1323
|
|
|
1301
|
-
// Get all message IDs for batch loading
|
|
1324
|
+
// Get all message IDs for batch loading artifacts
|
|
1302
1325
|
const messageIds = details.map(d => d.ID);
|
|
1303
1326
|
|
|
1304
|
-
// Batch load
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
//
|
|
1327
|
+
// Batch load input artifacts for these messages. Since the backfill migration
|
|
1328
|
+
// (V202605271400__Backfill_Attachment_Artifacts) converted all legacy
|
|
1329
|
+
// ConversationDetailAttachment rows to artifact pairs, the artifact junction
|
|
1330
|
+
// is the single source of truth — no separate attachment query needed.
|
|
1308
1331
|
const inputArtifactsByDetailId = await this.loadInputArtifactsBatch(messageIds, contextUser, provider);
|
|
1309
1332
|
|
|
1310
1333
|
// Build ChatMessage array with attachments and input artifacts
|
|
@@ -1312,69 +1335,9 @@ export class RunAIAgentResolver extends ResolverBase {
|
|
|
1312
1335
|
|
|
1313
1336
|
for (const detail of details) {
|
|
1314
1337
|
const role = this.mapDetailRoleToMessageRole(detail.Role);
|
|
1315
|
-
const attachments = attachmentsByDetailId.get(detail.ID) || [];
|
|
1316
|
-
|
|
1317
|
-
// Get attachment data with content URLs (handles both inline and FileID storage)
|
|
1318
|
-
const attachmentDataPromises = attachments.map(att =>
|
|
1319
|
-
attachmentService.GetAttachmentData(att, contextUser, provider)
|
|
1320
|
-
);
|
|
1321
|
-
const attachmentDataResults = await Promise.all(attachmentDataPromises);
|
|
1322
|
-
|
|
1323
|
-
// Decide inline vs. tools per-attachment via the artifact-type registry
|
|
1324
|
-
// and the pure RouteArtifact() function. See plans/artifact-attachment-unification.md.
|
|
1325
|
-
//
|
|
1326
|
-
// Note on the modality check: this resolver doesn't know which model will
|
|
1327
|
-
// ultimately run, so RouteArtifact's modality predicate is passed as a no-op.
|
|
1328
|
-
// Model-specific modality enforcement is the driver layer's responsibility;
|
|
1329
|
-
// a follow-up PR can thread the active model through here to surface modality
|
|
1330
|
-
// mismatches at the hard-error path described in plan §4.
|
|
1331
1338
|
const validAttachments: AttachmentData[] = [];
|
|
1332
1339
|
|
|
1333
|
-
for
|
|
1334
|
-
if (!result) continue;
|
|
1335
|
-
// Storage-unified attachments link forward to their artifact version
|
|
1336
|
-
// via ArtifactVersionID; the artifact path handles delivery, so skip
|
|
1337
|
-
// the attachment row here to avoid double-processing.
|
|
1338
|
-
if (result.attachment.ArtifactVersionID) {
|
|
1339
|
-
continue;
|
|
1340
|
-
}
|
|
1341
|
-
const mime = result.attachment.MimeType || '';
|
|
1342
|
-
const fileName = result.attachment.FileName ?? '';
|
|
1343
|
-
const ext = fileName.includes('.') ? fileName.split('.').pop() : undefined;
|
|
1344
|
-
const artifactType = ArtifactMetadataEngine.Instance.GetArtifactTypeByMimeType(mime, ext);
|
|
1345
|
-
|
|
1346
|
-
const decision = RouteArtifact({
|
|
1347
|
-
typeDefault: artifactType?.DefaultDeliveryMode ?? 'ToolsOnly',
|
|
1348
|
-
forceToolsOnly: false,
|
|
1349
|
-
mimeType: mime,
|
|
1350
|
-
sizeBytes: result.attachment.FileSizeBytes ?? 0,
|
|
1351
|
-
inlineSizeCap: INLINE_SIZE_CAP,
|
|
1352
|
-
modelSupportsModality: () => true,
|
|
1353
|
-
modelName: '<resolver>',
|
|
1354
|
-
artifactTypeName: artifactType?.Name ?? mime,
|
|
1355
|
-
});
|
|
1356
|
-
|
|
1357
|
-
if (decision.delivery !== 'inline') {
|
|
1358
|
-
// Tools or error: skip inline embedding. The agent reaches the
|
|
1359
|
-
// bytes via artifact tools. Driver layer handles modality enforcement.
|
|
1360
|
-
if (decision.delivery === 'tools' && decision.annotation) {
|
|
1361
|
-
LogStatus(`[RunAIAgentResolver] ${decision.annotation}`);
|
|
1362
|
-
}
|
|
1363
|
-
continue;
|
|
1364
|
-
}
|
|
1365
|
-
validAttachments.push({
|
|
1366
|
-
type: ConversationUtility.GetAttachmentTypeFromMime(result.attachment.MimeType),
|
|
1367
|
-
mimeType: result.attachment.MimeType,
|
|
1368
|
-
fileName: result.attachment.FileName ?? undefined,
|
|
1369
|
-
sizeBytes: result.attachment.FileSizeBytes ?? undefined,
|
|
1370
|
-
width: result.attachment.Width ?? undefined,
|
|
1371
|
-
height: result.attachment.Height ?? undefined,
|
|
1372
|
-
durationSeconds: result.attachment.DurationSeconds ?? undefined,
|
|
1373
|
-
content: result.contentUrl
|
|
1374
|
-
});
|
|
1375
|
-
}
|
|
1376
|
-
|
|
1377
|
-
// Get input artifacts for this message — same routing logic, plus ForceToolsOnly.
|
|
1340
|
+
// Get input artifacts for this message — routing via RouteArtifact.
|
|
1378
1341
|
const inputArtifacts = inputArtifactsByDetailId.get(detail.ID) || [];
|
|
1379
1342
|
for (const artifactVersion of inputArtifacts) {
|
|
1380
1343
|
const artifactMime = artifactVersion.MimeType || '';
|
|
@@ -16,6 +16,7 @@ import { LogError, LogStatus, UserInfo } from '@memberjunction/core';
|
|
|
16
16
|
import { TestEngine } from '@memberjunction/testing-engine';
|
|
17
17
|
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
18
18
|
import { PUSH_STATUS_UPDATES_TOPIC } from '../generic/PushStatusResolver.js';
|
|
19
|
+
import { startLivenessPulse } from '../generic/FireAndForgetHeartbeat.js';
|
|
19
20
|
import { TestRunVariables, TestLogMessage, TestRunResult as EngineTestRunResult } from '@memberjunction/testing-engine-base';
|
|
20
21
|
|
|
21
22
|
// ===== GraphQL Types =====
|
|
@@ -254,12 +255,17 @@ export class RunTestResolver extends ResolverBase {
|
|
|
254
255
|
userPayload: UserPayload,
|
|
255
256
|
user: UserInfo
|
|
256
257
|
): void {
|
|
258
|
+
// Keep-alive pulse so a long-running test never trips the client idle timeout.
|
|
259
|
+
// The client captures the testRunId from progress events for reconciliation.
|
|
260
|
+
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, resolver: 'RunTestResolver' });
|
|
261
|
+
|
|
257
262
|
this.executeTest(testId, verbose, environment, tags, variables, pubSub, userPayload, user)
|
|
258
263
|
.catch((error: unknown) => {
|
|
259
264
|
const errorMessage = (error instanceof Error) ? error.message : 'Unknown background test execution error';
|
|
260
265
|
LogError(`🔥 Fire-and-forget test execution failed: ${errorMessage}`, undefined, error);
|
|
261
266
|
this.publishFireAndForgetError(pubSub, userPayload, testId, errorMessage);
|
|
262
|
-
})
|
|
267
|
+
})
|
|
268
|
+
.finally(() => pulse.stop());
|
|
263
269
|
}
|
|
264
270
|
|
|
265
271
|
/**
|
|
@@ -343,6 +349,12 @@ export class RunTestResolver extends ResolverBase {
|
|
|
343
349
|
userPayload: UserPayload,
|
|
344
350
|
user: UserInfo
|
|
345
351
|
): void {
|
|
352
|
+
// Keep-alive pulse so a long-running suite never trips the client idle timeout.
|
|
353
|
+
// (Suite reconciliation isn't wired: suite progress carries the per-test run id, not the
|
|
354
|
+
// Test Suite Run id, so the client has no handle to reconcile against — the pulse is the
|
|
355
|
+
// protection here.)
|
|
356
|
+
const pulse = startLivenessPulse({ pubSub, sessionId: userPayload.sessionId, resolver: 'RunTestResolver' });
|
|
357
|
+
|
|
346
358
|
this.executeSuite(
|
|
347
359
|
suiteId, verbose, environment, parallel, tags, variables,
|
|
348
360
|
selectedTestIds, sequenceStart, sequenceEnd, pubSub, userPayload, user
|
|
@@ -350,7 +362,7 @@ export class RunTestResolver extends ResolverBase {
|
|
|
350
362
|
const errorMessage = (error instanceof Error) ? error.message : 'Unknown background suite execution error';
|
|
351
363
|
LogError(`🔥 Fire-and-forget suite execution failed: ${errorMessage}`, undefined, error);
|
|
352
364
|
this.publishFireAndForgetSuiteError(pubSub, userPayload, suiteId, errorMessage);
|
|
353
|
-
});
|
|
365
|
+
}).finally(() => pulse.stop());
|
|
354
366
|
}
|
|
355
367
|
|
|
356
368
|
// ===== Result Building =====
|